I recently wrote about using Full Text Search (FTS) in a Node.js application that makes use of Couchbase Server 4.5 and higher. As you may know, FTS is available as Developer Preview starting in Couchbase Server 4.5. This is huge for Couchbase because it opens the door when it comes to possibilities. Instead of doing inefficient wildcard queries, we can use search queries against full text indexes. The need to be using ElasticSearch or Solr becomes less necessary as well, unless of course your business is built around search.

We’re going to take the examples seen in the Node.js article and see them in action using the Go programming language. If you don’t care about Node.js or are unfamiliar with the example used, don’t worry. In the previous example we had an imaginary resume evaluation scenario where we could scan job applicant resumes for keywords and score them. We’ll continue to use that example.

Create a New Project with the Dependencies

To make this example easy to understand, we’ll be starting from scratch. That said, the assumption is that you already have GoLang installed and configured on your machine and that Couchbase Server 4.5 is up and running.

Create a new GoLang project. My project will be found at $GOPATH/src/github.com/nraboy/cbfts/main.go. Before we jump into the code, we need to install the Couchbase Go SDK. From the Command Prompt (Windows) or Terminal (Mac and Linux), execute the following:

go get github.com/couchbase/gocb 1 2 3 go get github . com / couchbase / gocb

At this point we’re ready to start developing, but before we do, we need to configure the necessary indexes on our Couchbase bucket.

Create a Full Text Search Index in Couchbase Server

Before you can start using full text search, you must create a special index. This can be done from the Indexes -> Full Text tab of the Couchbase administrative dashboard.

From this section you’ll want to choose New Full Text Index and choose the name and bucket to apply it on. For this example we’ll be using the default bucket and an index of resume-search. Our index will be very basic, so I encourage you to check out the documentation so you can best meet your needs.

After the index has been created, click the refresh button. At this point you can test it out via the dashboard or your own code.

Getting to Know the Example Data Model

With the index created let’s take a quick look at our data model. We know that this is going to be a resume-like document, so with a bit of imagination, we can come up with something like this:

{ "firstname": "Nic", "lastname": "Raboy", "skills": [ "java", "node.js", "golang", "nosql" ], "summary": "I am a cool guy working on cool things", "social": { "github": "https://www.github.com/nraboy", "twitter": "https://www.twitter.com/nraboy" }, "employment": [ { "employer": "Couchbase", "title": "Developer Advocate", "location": "San Francisco" } ] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { "firstname" : "Nic" , "lastname" : "Raboy" , "skills" : [ "java" , "node.js" , "golang" , "nosql" ] , "summary" : "I am a cool guy working on cool things" , "social" : { "github" : "https://www.github.com/nraboy" , "twitter" : "https://www.twitter.com/nraboy" } , "employment" : [ { "employer" : "Couchbase" , "title" : "Developer Advocate" , "location" : "San Francisco" } ] }

The above document has information such as skills, social media information, and employment history. Yes, a true job building application will probably have much more details, but this is only an example.

The scope of what we’re going to pay attention to is the text Developer Advocate. We’re going to write GoLang code for searching for that phrase.

Developing the Application for Data Searching

Because this project is a simple example, all the code we write will be in the single main.go file. Let’s start by importing all the dependencies to be used within the application:

package main import ( "encoding/json" "fmt" "github.com/couchbase/gocb" "github.com/couchbase/gocb/cbft" ) 1 2 3 4 5 6 7 8 9 10 11 package main import ( "encoding/json" "fmt" "github.com/couchbase/gocb" "github.com/couchbase/gocb/cbft" )

As you can see above, we’re including a JSON dependency for marshalling structures into JSON. We’re also importing the Couchbase and Couchbase FTS dependency.

The data we want to collect will be very specific. Because of this, it would be a good idea to create a new data structure to accommodate this. Let’s call this structure FtsHit and have it look like the following:

type FtsHit struct { ID string `json:"id,omitempty"` Score float64 `json:"score,omitempty"` } 1 2 3 4 5 6 type FtsHit struct { ID string ` json : "id,omitempty" ` Score float64 ` json : "score,omitempty" ` }

We’ll be keeping the document id that is found in the search as well as the score that the search returns on each hit. Each of these structure properties have their own JSON tags and they will be ignored if they are empty.

This brings us to the magic code found in our main function.

func main() { cluster, _ := gocb.Connect("couchbase://localhost") bucket, _ := cluster.OpenBucket("default", "") query := gocb.NewSearchQuery("resume-search", cbft.NewMatchQuery("developer advocate")) result, _ := bucket.ExecuteSearchQuery(query) var ftsHit *FtsHit for _, hit := range result.Hits() { ftsHit = &FtsHit{ID: hit.Id, Score: hit.Score} jsonHit, _ := json.Marshal(&ftsHit) fmt.Println(string(jsonHit)) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main ( ) { cluster , _ : = gocb . Connect ( "couchbase://localhost" ) bucket , _ : = cluster . OpenBucket ( "default" , "" ) query : = gocb . NewSearchQuery ( "resume-search" , cbft . NewMatchQuery ( "developer advocate" ) ) result , _ : = bucket . ExecuteSearchQuery ( query ) var ftsHit * FtsHit for _ , hit : = range result . Hits ( ) { ftsHit = & FtsHit { ID : hit . Id , Score : hit . Score } jsonHit , _ : = json . Marshal ( & ftsHit ) fmt . Println ( string ( jsonHit ) ) } }

Before we can begin searching we need to establish a connection to our Couchbase cluster and open a bucket. I’ll be connecting to a locally running node and to the default bucket. Once connected we can create a new full text search query using the previously created resume-search index. This query will look for the text developer advocate in all documents and in any property. Essentially, if the text exists anywhere in a document, the document will be counted as a hit.

With the query in hand, we can execute it and loop through the results. Each result will have certain bits and pieces placed into our FtsHit object where it will then be parsed into JSON and printed.

So far so good right?

Well what happens when you want to be a bit more specific in your search? While it is convenient and cool to be able to search an entire document, maybe we only want to search within the employment history.

We can make an adjustment to the actual SearchQuery with little effort:

query := gocb.NewSearchQuery("resume-search", cbft.NewMatchQuery("developer advocate").Field("employment.title")) 1 2 3 query : = gocb . NewSearchQuery ( "resume-search" , cbft . NewMatchQuery ( "developer advocate" ) . Field ( "employment.title" ) )

While we tried to keep this sample simple, there are a ton of other cool things you can do with full text search. Maybe you want to expand on the search conditions to be more specific, or maybe you want to use an index that is more specific. More information on FTS can be found in the Couchbase developer documentation.

Conclusion

We just saw how easy it was to make use of full text search (FTS) in Couchbase using the Go programming language. By using FTS, we can search within documents more efficiently than using queries that include a bunch of wildcards.

To see more on FTS, visit the Couchbase Developer Portal.