{"id":141,"date":"2010-12-27T19:59:06","date_gmt":"2010-12-28T00:59:06","guid":{"rendered":"http:\/\/blogs.law.harvard.edu\/dlarochelle\/?p=141"},"modified":"2017-11-27T13:26:22","modified_gmt":"2017-11-27T18:26:22","slug":"using-postgresql-for-an-ajax-autocomplete-field","status":"publish","type":"post","link":"https:\/\/archive.blogs.harvard.edu\/dlarochelle\/2010\/12\/27\/using-postgresql-for-an-ajax-autocomplete-field\/","title":{"rendered":"Using PostgreSQL for an Ajax Autocomplete Field"},"content":{"rendered":"<h1 id=\"internal-source-marker_0.8348402467090636\">Using PostgreSQL for an Ajax Autocomplete Field<\/h1>\n<p>I recently implemented an Ajax based autocomplete field for a web application using a PostgreSQL database on the back-end. I had trouble finding appropriate PostgreSQL queries, so I\u2019ve decided to share my approach in the hope that it will be useful to others.<\/p>\n<h1>Data Format<\/h1>\n<p>The application uses the following PostgreSQL table to store the author data:<\/p>\n<p style=\"padding-left: 30px\"><code>CREATE TABLE authors (<br \/>\nauthors_id serial PRIMARY KEY,<\/code><br \/>\n<code>author_name character varying UNIQUE NOT NULL<\/code><br \/>\n<code>);<\/code><\/p>\n<p>The author_name field represents the full name of an author. E.g. \u2018jane austen\u2019, \u2018john smith\u2019, \u2018john q. public\u2019, \u2018william shakespeare\u2019, etc.<\/p>\n<h1>Client Side JavaScript<\/h1>\n<p>On the client side, the application uses the jQuery UI Autocomplete widget.(<a href=\"http:\/\/jqueryui.com\/demos\/autocomplete\/\">http:\/\/jqueryui.com\/demos\/autocomplete\/<\/a>) jQuery is a great library and if you\u2019re creating dynamic web pages and not using it, you really should be. However, jQuery has been well documented so I\u2019m not going to discuss the details of the client side implementation. Additionally, the back-end code is general enough that it should work with other JavaScript libraries with minimal modifications. Essentially, when the user begins typing, the jQuery UI Autocomplete widget makes a get request to the server with the text the user entered and expects the server to respond with a json object containing the results to show the user.<\/p>\n<h1>Initial Implementation<\/h1>\n<p>Creating the initial version of the autocomplete field was relatively straightforward. To handle the Ajax we simply added the following method to our controller:<\/p>\n<p style=\"padding-left: 30px\"><code>use JSON;<\/code><br \/>\n<code>sub json_author_search : Local<\/code><br \/>\n<code>{<\/code><br \/>\n<code>my ( $self, $c, $dashboards_id ) = @_;<\/code><br \/>\n<code> my $term = $c-&gt;req-&gt;param( 'term' ) || 0;<\/code><br \/>\n<code> $term = '%' . $term . '%';<\/code><br \/>\nmy $terms =<br \/>\n<code>$c-&gt;dbis-&gt;query( \"select authors_id, author_name as label from authors where author_name ilike ? \", $term )-&gt;hashes;<\/code><br \/>\n<code> $c-&gt;res-&gt;body( encode_json( $terms ) );<\/code><br \/>\n<code> return;<\/code><br \/>\n<code> }<\/code><\/p>\n<p>This code was fully functional and worked with small data sets on a development machine. However, once we started testing on our production machine with a large dataset, it became clear that there were performance problems.<\/p>\n<p>Using an explain showed that PostgreSQL was doing a sequential scan. I.e. it was reading through the entire database table for every query:<\/p>\n<p style=\"padding-left: 30px\"><code>mediacloud=# explain select authors_id, author_name as label from authors where author_name ilike '%bla%';<\/code><br \/>\n<code>QUERY PLAN<\/code><br \/>\n<code>---------------------------------------------------------<\/code><br \/>\n<code>Seq Scan on authors (cost=0.00..37.33 rows=1 width=22)<\/code><br \/>\n<code>Filter: ((author_name)::text ~~* '%bla%'::text)<\/code><br \/>\n<code>(2 rows)<\/code><\/p>\n<h1>Possible Solutions<\/h1>\n<p>I did some web searches looking for a solution. I found Nikolay Samokhvalov\u2019s slides on Using PostgresSQL In Web 2.0 Applications.(http:\/\/www.scribd.com\/doc\/4844027\/Using-PostgreSQL-In-Web-20-Applications-)<br \/>\nThese slides have useful information and are worth reading. However, their suggested approaches focus on key word search rather than autocomplete. The first approach that Samokhvalov gives is to use prefix search. This solution uses like \u2018bla%\u2019 queries and relies on text_pattern_ops indexes for quick performance. Samokhvalov\u2019s other tip is to use lower instead of using ilike. So in our case we would add the following index:<\/p>\n<p style=\"padding-left: 30px\"><code>CREATE INDEX author_name_prefix ON authors USING btree ( lower (author_name) text_pattern_ops);<\/code><\/p>\n<p>and change our search query to:<\/p>\n<p style=\"padding-left: 30px\"><code>$term = \u2018%\u2019 . $term;<br \/>\n$c-&gt;dbis-&gt;query( \"select authors_id, author_name as label from authors where lower(author_name) like ? \", $term )-&gt;hashes;<\/code><\/p>\n<p>This approach is fast but it doesn\u2019t give us the behavior that we wanted. For example, if the user typed \u2018sm\u2019, we would want \u2018john smith\u2019 to be included in the search results. However, \u201cjohn smith\u201d would only be matched if the user instead typed \u2018jo\u2019.<\/p>\n<p>Another solution that Samkhvalov suggests is using GIN indexes with full text search. This is an interesting approach but it didn\u2019t seem like the right fit for an autocomplete service. As far as I was able to determine, PostgreSQL\u2019s full text search is only designed to match entire words so \u2018sm\u2019 would not match \u2018smith\u2019. Finally Samkhvalov suggests an interesting hybrid approach that uses both prefix search and full text search with GIN indexes. This approach relies on having a separate table that contains all the distinct words in every string in the table that\u2019s being searched. So in our case we would have an author words table that contained words such as \u2018alice\u2019,\u2019bob\u2019,\u2019brown\u2019,\u2019david\u2019,\u2019smith\u2019,etc. This approach might work well for relatively static data &#8212; creating the initial table of distinct words is easy. However, in our data set we are constantly adding new author strings. I wanted to avoid the additional complexity of managing and updating an author words table.<\/p>\n<h1>My Solution<\/h1>\n<p>I added the following indexes to the authors table:<\/p>\n<p style=\"padding-left: 30px\"><code>create index authors_name_varchar_pattern on authors(lower(author_name) varchar_pattern_ops);<br \/>\ncreate index authors_name_varchar_pattern_1 on authors(lower(split_part(author_name, ' ', 1)) varchar_pattern_ops);<br \/>\ncreate index authors_name_varchar_pattern_2 on authors(lower(split_part(author_name, ' ', 2)) varchar_pattern_ops);<br \/>\ncreate index authors_name_varchar_pattern_3 on authors(lower(split_part(author_name, ' ', 3)) varchar_pattern_ops);<\/code><\/p>\n<p>Then I changed the query to:<\/p>\n<p style=\"padding-left: 30px\"><code>$term = $term . '%';<br \/>\nmy $terms =<br \/>\n$c-&gt;dbis-&gt;query( \"select authors_id, author_name as label from authors where lower(author_name) like lower(?) OR \" .<br \/>\n\" lower(split_part(author_name, ' ', 1)) like lower(?) OR \" .<br \/>\n\" lower(split_part(author_name, ' ', 2)) like lower(?) OR \" .<br \/>\n\" lower(split_part(author_name, ' ', 3)) like lower(?) LIMIT 100 \",<br \/>\n$term, $term, $term, $term )-&gt;hashes<\/code><\/p>\n<p>The above query performs prefix searches on each of the first three words within the author name string. Additionally, it performs a prefix search on the entire author name string. We OR these prefix searches so that we return the results that match any of the prefix searches. The nice thing about this search is that the user can start typing either the first name or the last name (or the middle name). For example, when the user starts typing \u2018joh\u2019, they\u2019ll get a list that includes the \u2018john\u2019 names. When the user starts typing \u2018smi\u2019, they\u2019ll get a list that includes the \u2018smith\u2019 names. Because we also do a prefix search on the entire string, the user could start typing \u2018john\u2019 to get a list of names starting with \u2018john\u2019 and then expand that to \u2018john sm\u2019 to get a list of \u2018john smith\u2019s and similar names.<\/p>\n<p>(Note: it might have been safe to omit <code>lower(split_part(author_name, ' ', 1)) like lower(?)<\/code> from the query since it will often be redundant with <code>lower(author_name) like lower(?)<\/code>. We decided to leave it in place to enhance readability and out of concern that it may be necessary to handle improperly formatted author strings.)<\/p>\n<h1>Conclusion<\/h1>\n<p>This approach does have its weakness. It won\u2019t work for names involving more than 3 words. Fortunately those names are rare. It would also have been nice to be able to do full wild card searches such as like \u2018%bla%\u2019. Unfortunately, PostgreSQL does not have any index to optimize those types of searches.<\/p>\n<p>In an ideal world, the database would have a set of indexes and operators that would perfectly handle the search queries necessary for autocomplete and abstract them away from the programmer. However, I\u2019ve presented a solution that has performed well for me and has added only a minimal amount of complexity to the code and the database schema. I\u2019d love to hear comments or suggestions for alternate approaches.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Using PostgreSQL for an Ajax Autocomplete Field I recently implemented an Ajax based autocomplete field for a web application using a PostgreSQL database on the back-end. I had trouble finding appropriate PostgreSQL queries, so I\u2019ve decided to share my approach &hellip; <a href=\"https:\/\/archive.blogs.harvard.edu\/dlarochelle\/2010\/12\/27\/using-postgresql-for-an-ajax-autocomplete-field\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":2148,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[260,1],"tags":[8685],"class_list":["post-141","post","type-post","status-publish","format-standard","hentry","category-tech","category-uncategorized","tag-geeky"],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/archive.blogs.harvard.edu\/dlarochelle\/wp-json\/wp\/v2\/posts\/141","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/archive.blogs.harvard.edu\/dlarochelle\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/archive.blogs.harvard.edu\/dlarochelle\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/archive.blogs.harvard.edu\/dlarochelle\/wp-json\/wp\/v2\/users\/2148"}],"replies":[{"embeddable":true,"href":"https:\/\/archive.blogs.harvard.edu\/dlarochelle\/wp-json\/wp\/v2\/comments?post=141"}],"version-history":[{"count":15,"href":"https:\/\/archive.blogs.harvard.edu\/dlarochelle\/wp-json\/wp\/v2\/posts\/141\/revisions"}],"predecessor-version":[{"id":156,"href":"https:\/\/archive.blogs.harvard.edu\/dlarochelle\/wp-json\/wp\/v2\/posts\/141\/revisions\/156"}],"wp:attachment":[{"href":"https:\/\/archive.blogs.harvard.edu\/dlarochelle\/wp-json\/wp\/v2\/media?parent=141"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/archive.blogs.harvard.edu\/dlarochelle\/wp-json\/wp\/v2\/categories?post=141"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/archive.blogs.harvard.edu\/dlarochelle\/wp-json\/wp\/v2\/tags?post=141"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}