Archive for the ‘Search’ Category

Mobile Has Unbottled the UX Genie

Tuesday, May 24th, 2011

I think it was the editor of Wired magazine who said something like “Designing for the iPad is liberating, because users have no pre-conceived expectations for the user experience”.  He hit the nail on the head, because right now many of the most exciting user experience innovations are occurring on mobile.  I wanted to share a few:

1) Do@ is an iPhone app which does search better by replacing text search results with apps* pre-filled with result sets, and a WebOS-style flick left or right between the apps.  By “outsourcing” the results display to 3rd-party apps, Do@ leaves plenty of room for innovation, without losing value as an aggregator of all apps appropriate for each category (e.g. @movies,  @news, @music).

2) Siri has the theory & user experience right for voice search.  In my testing, the app isn’t ready for prime time because it makes too many mistakes.  But with Apple’s purchase, we’ll likely see pieces of the technology built right into future releases of iOS.

3) Windows Phone 7 Mango has added Google-Goggles-style “Bing Vision” search built right into the OS.  Also, they’re building closer integration between Bing search results and results within apps like IMDB.

4) WebOS Just Type extends search with Quick Actions, once again proving that innovation can still help reduce the number of steps required to perform common tasks

You’ll notice a trend here: I’m showing examples of innovations for search.  As you may have noticed in my blog, I’m passionate about the synergy offered by combining mobile innovations and search innovations.  Since our focus at Avalon Consulting, LLC. is content, specifically leveraging your content to improve your users’ experiences, our mobile focus is naturally around delivering your content to more people more often using the most effective mobile user experiences.  I believe we’ve only scratched the surface of how the combination of mobile devices and search technologies will make valuable content more accessible.

* term used loosely here since they’re HTML5 and not independently installed on the device

Auto-Complete in MarkLogic Server

Wednesday, March 30th, 2011

It Must Be Insanely Fast and Meet the Requirements

Auto-complete is hard to optimize because:

  1. we’re searching on partial-words, but search engines are not optimized for that
  2. queries happen on each keystroke, so expect five to ten times your normal search query traffic in auto-complete queries
  3. if response times are slower than 100 milliseconds, users are on to the next keystroke before you show your suggestions

Another complication is that each implementation has different requirements for defining and matching the set of possible values for auto-complete suggestions.  For this article I’m assuming the most common requirements: given a defined set of possible values, we want to present as auto-complete suggestions all values which contain any word or phrase that starts with the text being entered by the user, while matching case-insensitively.

Until now, I had been using custom Java to achieve scalable auto-complete that can match any word in the set of possible values, but I’ve now found a way to do it in MarkLogic server.  But before I explain the new approach, let’s quickly review the solutions I tried which didn’t work.

Basic Element Range Indexes Don’t Meet the Requirements

Using MarkLogic Server, standard Element Range Indexes and/or search:suggest are powerful options for auto-complete, as mentioned in my previous blog post on the topic: Scalable Search Auto-Complete.  But they only meet a narrow set of use cases.  The main limitation is they can only do a “starts-with” match against the entire value.  They don’t support starts-with against any word in the set of possible values, which is a more common requirement.  So if a user starts typing “ban” they will get matches for “Bank of America” but not “Wells Fargo Bank”.

auto-complete mockup 1

XQuery example 1, “basic element-value match”, is fast but only meets a narrow set of use cases:

cts:element-value-match(xs:QName("COMPANY"),"ban*", "limit=50")

Wild-Cards aren’t Fast Enough

Wild-cards present a solution, but as in all technologies wild-cards are one of the slowest query types.  While wild-card searching in MarkLogic Server is very fast, I have not been able to tune it to be scalable enough for auto-complete.  I’ve used 1, 2, and 3-character indexes to enable fast matching using wild-cards even on the first keystrokes.  Under load I’d like at least 90% of queries below 100 ms to meet #3 above, but I’m seeing wild-card queries take as much as 1 second at the 90% line.  I’ve also tried wild-card queries in fields and elements, with very similar results.

XQuery example 2, “wild-card search”, is not fast enough:

(cts:search(/DOCUMENT/COMPANY, "ban*")/text())[1 to 50]

fn:contains isn’t Fast Enough

XPath has fn:contains which matches any sub-string, thus coming close to the required functionality, but I didn’t expect high performance because it has no index to optimize the query–it essentially conducts a full-text scan.  While I was impressed that MarkLogic’s implementation of fn:contains in vanilla XPath performs even better than wild-card queries, the need to be case-insensitive adds a serious performance hit, making this the slowest query of the bunch.

XQuery example 3, “contains”, is not fast enough:

(cts:element-values(xs:QName("COMPANY"))[contains(lower-case(.),"ban")]/text())[1 to 50]

Expanded Word Queries are Very Slow Under Load

I tried some very creative solutions, including wild-card matching of all possible words in the text, then using the words for a word query.  I expected this to be highly optimized because word queries use the search index and are very fast.  Either I did something wrong, or sending so many words in one “or” query slows things down.  Under load, these queries were even slower than contains queries.

XQuery example 3, “expanded words query”, is not fast enough:

let $words := cts:element-word-match(xs:QName("COMPANY"),"ban*")
let $wordQuery := cts:or-query((for $word in $words return $word))
return (cts:search(/DOCUMENT/COMPANY, $wordQuery))[1 to 50]

Chunked Element Range Indexes Are the Answer

Luckily, I landed on a solution that works wonderfully.  It allows matching against any word in the values, and performs very quickly: under 10 milliseconds with no load, and under 150 milliseconds with outrageously high load (100 concurrent thread in JMeter).  I create what I’ll call a “chunked” Element Range Index which takes advantage of the indexes’ optimization for “starts-with” queries by creating a new value for each word in each value.  In order to associate the partial “chunked” values to the full value, I just append the full value after a colon.  So for “Wells Fargo Bank” I would create three values: “Wells Fargo Bank:Wells Fargo Bank”, “Fargo Bank:Wells Fargo Bank”, and “Bank:Wells Fargo Bank”.  This way when a user types “ban” and I search for values starting with “ban”, one of my matches will be “Bank:Wells Fargo Bank” and I will then strip everything before the colon and present the user with the match of “Wells Fargo Bank”.

auto-complete mockup 1

XQuery example 4, creates “chunked” values on which we can create a “chunked Element Range Index”:

(: This differs from tokenize because we want the whole trailing
 : string with each token :)
declare function local:chunkOnNonWordChars($value as xs:string, $fullValue as xs:string) {
  (: only shrink the value if it contains whitespace character(s)
   : between non-whitespace characters :)
  if ( matches($value, "^\w+\W+\w") ) then (
    (: remove the first word and following whitespace characters :)
    let $smallerValue := replace($value, "^\w+\W+", "")
    return (
      concat($value, ":", $fullValue),
      local:chunkOnNonWordChars($smallerValue, $fullValue)
    )
  ) else (
    concat($value, ":", $fullValue)
  )
};

xdmp:document-insert("autocomplete-seed.xml", <xml>{
  for $value in cts:element-values(xs:QName("COMPANY"))
  return
    for $chunk in local:chunkOnNonWordChars($value, $value)
    return <AUTOCOMPLETE_COMPANY>{ $chunk }</AUTOCOMPLETE_COMPANY>
}</xml>)

XQuery example 5, “chunked element-value match”, is fast and meets the requirement to match any word from the values:

let $matches := cts:element-value-match(xs:QName("AUTOCOMPLETE_COMPANY"), "ban*", "limit=50")
let $clean_matches := for $chunk in $matches return replace($chunk, ".*:", "")
return distinct-values($clean_matches)

Advice From an Expert

I had this article reviewed by Colleen Whitney, the MarkLogic engineer who wrote (and performance optimized) search:suggest, and she provided some tips compiled from multiple engineers on her team.  We can all use these when performance tuning our matches against element range indexes:

The major take-away is that on very large datasets, resolving matches is highly data-dependent. The factors (in order of importance) are:

  • Number of total values with the same prefix as the match pattern. (I think this is a key issue given your approach.)
  • Collation.
  • Average number of characters in each value. (Longer strings are slower to compare; also if the beginnings of the strings are similar, comparison will be slower)
  • Number of unique values.

Therefore, users can improve performance on large datasets in several ways:

  • Be selective about which index(es) drive suggestions.
  • Design interaction with the UI so that suggestions aren’t given until there are a minimum of 3 characters as input to the lexicon call.
  • Use codepoint collation on string range indexes.

For most use cases, I don’t think the data sets are large enough to require a user experience limitation of only showing suggestions after 3 characters of input.  But understanding Colleen’s tests were run on data sets with up to 22 million records, I’ll have to agree that with data sets that large we’ll most likely have to make some compromises on user experience.

Full-Disclosure on My Testing Approach

My testing approach matches my previous post, using geonames.org data (though about 85,000 cities loaded this time).  I’m also still running a set of queries as if users were typing each character of each city name.  My hardware is now a Dell E6510 with 4 GB ram and Windows 7.

Here’s a snapshot of my JMeter results after one of my tests (only 30 threads looping 10 times each):

JMeter load test results screenshot

The times above are in milliseconds.  While you’ll notice that the median timings are acceptable (under 100 milliseconds) for most of the approaches, with a few outlier slow queries throwing off the averages, the 90% line is where I look to make sure the approach will have acceptable timings most of the time.  For this test run, only element-value-match and chuncked element-value match keep 90% of the responses under 100 milliseconds.  As I increase the size of the data or increase the load, the performance gap widens and it becomes more obvious that chunked element-value-match is much more scalable.

Other Requirements to Meet

While this solution is exciting, there are many potential auto-complete requirements beyond the scope this article, for example:

  • We may want to broaden the matches based on synonyms or spelling corrections.
  • Some user experiences may not need matches on the first few keystrokes, which significantly reduces the performance requirements.
  • We don’t want to query on every keystroke when keystrokes occur in quick succession.  Many javascript auto-complete libraires like YUI Auto-complete take care of this for us.
  • If we have a pre-defined order among the potential set of values, we would like to present our matches in the right order.
  • We might first show matches which start with the user’s text, then matches containing words or phrases which start with the users text.
  • We may want to group auto-complete suggestions, so we may need to create multiple chunked element range indexes, and run a query against each.  This would obviously add more overhead and require additional performance testing.

To meet many of these requirements, we’ll need to extend this approach.  For others we’ll need to fall-back to the approach of using custom Java code.

Conclusion

While the Java solution is still a viable alternative, it’s nice to now have a pure MarkLogic / XQuery solution for environments that can’t easily set up and manage a Java app server just for auto-complete.

Yes, Faceted Search for Mobile is Possible

Wednesday, May 12th, 2010

Faceted Search is recognized as a powerful search experience enhancement, easy enough for even novice users to filter large result sets.  As I’ve seen the increasing importance of mobile computing, I’ve struggled with how to fit facets on smaller mobile screens.  You see, facets usually belong on the left of search results, but I simply can’t see how to fit the facets there.  While some websites show facets above results, I can’t recommend this either as it would push down the all-important search results.

Luckily, as I investigated the excellent auto-complete implementation by MarkLogic, I had an inspiration.  You see, MarkLogic implemented their auto-complete directly off facet names and facet values.  And while this can be a great user experience on standard browser-based search experiences, in a mobile context the value is exponentially higher for two reasons:

  1. As mentioned above, screen real-estate is limited on mobile.  Auto-complete takes no extra screen real-estate since it floats and only shows as the user types.
  2. Typing on mobile devices is often more awkward, so a great auto-complete experience can save keystrokes.

Here’s a video I created using our Unified Search Platform which shows the user experience I’m describing:

Scalable Search Auto-Complete

Thursday, April 15th, 2010

For us search integrators, auto-complete is one of our most enjoyable challenges.  But make no mistake, implementing scalable search auto-complete can be a challenge.

Search Auto-Complete (aka instant search, find-as-you-type, look-ahead, predictive search, and many other names) is becoming more popular because if done right it can be great for end users, but the performance is no small issue since it has to respond for *every key stroke* of every user, and if it doesn’t respond almost instantly it still bogs down your servers but doesn’t really help end users.  So a search auto-complete solution must be:

  1. responsive (sub 100 ms) – so users see responses as they type
  2. high throughput (5-10x your existing search traffic) – so it can handle every keystroke for every user

Our most recent auto-complete project was for one of the most recognizable names in the financial industry.  For their implementation we had to execute auto-complete on over 50,000 items, and while auto-complete obviously only works well if it’s *very fast* (less than 100 ms), we also had to make it scalable enough for a very high traffic site.  50,000 items is too much to send to the web browser to do my favorite approach, client-side auto-complete, so we had to use AJAX in this case.  To make a long story short, we found that the most responsive, scalable, and flexible solution was to search strings in memory in the application layer (Java in this case).  No, we didn’t go back to the search engine for each keystroke, because for this use case it simply wasn’t responsive nor scalable enough.

I keep looking closely at offerings from search vendors to see if they provide a more packaged solution than the Java approach we used, and I’m excited to report what I’ve found in MarkLogic.   Our enterprise web team has been heavy into MarkLogic work, and I decided to experiment with that platform.  I found a gem called search:suggest which has two things I like a lot:

  1. It provides an uncommonly great user experience starting down the path of a demo I’ve long looked at as the future of advanced auto-complete
  2. My testing shows it is *very responsive and scalable*

My results with 50 concurrent JMeter threads show:

  • 23 ms average response time with 411 ms max response time
  • 82 qps average throughput

I must say I’m impressed!

For another comparison point, I implemented auto-complete the way a large MarkLogic client showed me they were doing it, using cts:element query, with a wildcard (*) appended to each search item.  It didn’t do nearly as well as search:suggest, with average response times taking several seconds, and max response times over 11 seconds.  So, no big surprise here . . . standard wildcard searches are expensive.  Good thing MarkLogic offers us search:suggest.

These were quick tests, and I hope to explore this with much more rigor, but I had to share my initial findings.  If you want to understand more details about what I did, feel free to reach out and I can provide more details.  Briefly, here’s what I did:  I pulled out an old JMeter configuration I’ve used in the past to test auto-complete.  It’s based on a database culled from geonames.org, with a test script matching one letter at a time for each city name, mimicking a user typing the city names . . . know I was matching over 20,000 items, with several keystrokes per item, and a configuration to *hammer* the system to test performance and maximum throughput.