Indeedのオープンソース A/B テスト・フレームワーク

今この瞬間にも、Indeed では 100 件以上の A/B テストを実行しています。

これらのテストは、 Proctor と呼ばれる自社開発したシステムによって管理されていますが、この Proctor がオープンソース化されました。

何年にも渡りA/Bテストを行ってきた私たち の経験を活かして、Proctor は開発されました。

そんなProctorのデザインおよび機能は、以下をまず目標に掲げています。

  • 簡単、高速、そして安全にテストを調整できること
  • トラフィックを詳細に絞り込んでセグメント化できること
  • テストの挙動から A/B テストの管理を切り離せること
  • 私たちの使用する様々なアプリケーション、サービス、ツール全てにおいて、対応可能であること
  • 複数のアプリケーション間でテストを同期できること

Proctor は多くの Indeed のプロダクトと統合されており、 Indeed のシステム全体の能力をパワフルにしました。さらに、 Proctor の本来の用途であったテスト以外に、動的な「フィーチャー・トグル(実装のオンオフ機能)」を介して、システム全体の挙動の入念な管理を可能にしました。これにより、安全かつ段階的に開発システムへの変更を適用できるようになったのです。

Proctor は Java で書かれており、 Java 仮想マシン上でホストされているどんな言語からでも使用が可能です。

Proctor のソースコードは GitHub (github.com/indeedeng/proctor) にホストされており、皆さんご自身のアプリケーション上での Proctor をご活用いただく方法を解説したドキュメンテーションリファレンス実装もご覧いただけます。そして、 Google Group では、Proctorについてディスカッション・質問を受け付けております。

また、定期開催している @IndeedEng テックトークの一環として Managing Experiments and Behavior Dynamically with Proctor (Proctorを使用したテスト管理と挙動の動的な管理) というテックトークも行いました。
Indeed だけでなく、 Proctor が皆さんのお役に立てば幸いです。

indeed_proctor_github

Indeed College Tour: On the Road in Search of Indeed’s Next Hires

Jobby the job search penguinHi, I’m Jobby. You might know me from Indeed’s Antarctica site. A team of our engineers created Indeed’s Antarctica job search site and me as an April Fool’s Day prank in 2010. That little prank turned out to be useful to job seekers, so Indeed Antarctica and I got to stick around! In fact, I made an encore appearance this April 1, 2013, on Indeed’s search results page as a Clippy-inspired job search assistant.

In the Fall 2013 and Spring 2014, I will hit the road with Indeed’s University Relations team and don my collegiate gear as we visit 11 colleges in the U.S. and Canada in search of top engineering talent for full-time and summer internship opportunities. During our university visits, we attend career fairs and hold on-campus interviews with computer science, engineering, and technical operations candidates.

“We have a high bar for hiring, so we go to the top programs to identify the top computer science and technical operations candidates,” says Jolynn Cunningham, Director of Talent at Indeed.

Want to know which schools I’m visiting in Fall 2013 and Spring 2014? See if you recognize me masquerading as your school’s mascot in our t-shirt design.

Indeed University Recruiting t-shirt

Now hiring:  Summer interns & new grads

University recruiting has been a priority at Indeed since its inception. In fact, one of Indeed’s first interns, Andrew Hudson, has been with Indeed for 8 years and is now our CTO. A key goal of our internship program is to get an intern’s code into production within their first week. This code often becomes a part of our systems and is used every day. Examples of past intern projects include:

  • Resume instant search
  • Job search country launches
  • Android file upload
  • Company Pages photo uploading, viewing, & data management

All of our teams at Indeed have recent college grads on them. Many of our key technologies and products were built by recent college grads and all of them are building great software to help people find jobs. Indeed’s college hires are paired with a mentor on their team who provides guidance on process and development during a new hire’s first few months.

From 24-hour hack-a-thons to ping pong tournaments to an end-of-summer boat party on Lake Travis, all Indeeders enjoy the fun, active culture of our Austin office. Our University Relations team puts together events to help interns and new hires quickly acclimate to the Austin, Texas, lifestyle. Past summer intern events include segway tours of downtown Austin, swimming at Barton Springs Pool, and horseback riding at a dude ranch.

How to apply

Check with your campus career services office to find out if Indeed will be at your career fair, and be sure to drop by the Indeed table, meet us, and pick up a t-shirt.

If Indeed is not visiting your campus, don’t worry. Indeed looks for talent everywhere, and all jobs for summer internship and full-time, new graduate positions are available here.

Connect with Indeed

Take a picture in your Indeed t-shirt and tweet it to @IndeedEng with #jobbytour

Or, check out our @IndeedEng Tech Talk series on our YouTube channel.

Building Resume Instant Search

[Editor’s Note: This post is a companion piece to a recent @IndeedEng talk. Slides and video are available.]

Our resume instant search feature allows employers to see search results as they type, helping them efficiently connect with job seekers. Google found that Instant saved users 2-5 seconds per search. To prevent undesired search results for partially-typed queries, we also had to implement an auto-complete feature that predicts a user’s query before they finish typing it. Both of these features must be lightning-fast in order to be effective.

The combination of the instant and auto-complete features allows us to deliver a streamlined search to our users. We know from testing that the faster we deliver results to users, the more time they spend doing what matters — reading resumes and contacting applicants.

Building Instant Search

One of our goals for the architecture of Instant Search was to build a clean, modular JavaScript application. Our use of the Google Closure framework helped us achieve this goal. The Google Closure tools include a dependency manager, a feature-rich JavaScript library, a JavaScript compiler, and templates that compile to both Java and JavaScript. We used these tools and implemented an event-driven architecture, resulting in a modular, easy-to-maintain application.

In event-driven models, program flow is based on events such as a mouse click or a key press. A common example in web development is the onclick handler, which allows developers to capture the click event on a DOM element. The element being clicked has no knowledge of the code that runs as a result of the click. It fires an event to signal it has been clicked, and the browser dispatches that event to all components that have registered to receive it.

Instant Search has four components:

  • AutoComplete – offers search query completions based on what the user has typed.
  • Instant – retrieves and renders resume search results.
  • Preview – retrieves and renders a preview of the resume on mouseover.
  • Result Page – the main dispatcher; the only component aware of the other components.

Each component fires off events as they occur without knowing anything about the other components on the page. For example, AutoComplete will fire an event when the search completion changes, and that event is handled by the Instant component, which retrieves results. The Result Page component listens for all events and dispatches them to the registered components.

Here is example code that shows the Result Page component listening for a set of events from the AutoComplete component:

// Attach events for Query AutoComplete
goog.events.listen(this.queryAutocomplete_,
  indeed.AutoComplete.EventType.QUERY_TYPE,
  this.handleQueryChange_, false, this);
goog.events.listen(this.queryAutocomplete_,
  indeed.AutoComplete.EventType.QUERY_CHANGE,
  this.handleQueryChange_, false, this);
goog.events.listen(this.queryAutocomplete_,
  indeed.AutoComplete.EventType.ENTER,
  this.handleQueryEnter_, false, this);
goog.events.listen(this.queryAutocomplete_,
  indeed.AutoComplete.EventType.TAB,
  this.handleQueryTab_, false, this);
goog.events.listen(this.queryAutocomplete_,
  [indeed.AutoComplete.EventType.SELECT,
    indeed.AutoComplete.EventType.NEW_SUGGESTIONS],
  this.handleQuerySelect_, false, this);
goog.events.listen(this.queryAutocomplete_,
  indeed.AutoComplete.EventType.FOCUSIN,
  this.handleQueryFocusIn_, false, this);
goog.events.listen(this.queryAutocomplete_,
  indeed.AutoComplete.EventType.FOCUSOUT,
  this.handleQueryFocusOut_, false, this);

The function handleQueryChange_ passes the text of the query to the Instant component:

/**
 * Handles changes to the query by notifying Instant
 * @param {indeed.events.TextEvent} e Change event.
 * @private
 */
indeed.Search.prototype.handleQueryChange_ = function(e) {
  if (this.instant_) {
    this.instant_.newQuery(e.text);
  }
};

All other components on the page act in a similar manner — Instant fires an event when it has new results, and Preview fires an event when it displays a resume. The architecture of the application ends up looking like this:

Figure 1: Result Page is composed of high-level components, including AutoComplete and Instant, which in turn are composed of lower-level handlers and renderers.

Figure 2: How AutoComplete handles events from its sub-components

This event-driven approach allows us to add new components to the search page without having to modify the components that were already on the page. Since the components have no knowledge of the Result Page, we have been able to package them into a library for use in other projects.

Navigation and Search Engines

If we just used JavaScript-triggered requests to populate search results and did not change the URL, our users would never be able to save or share specific search result pages, and search engines like Google could not crawl these pages for indexing.

In our initial implementation of Instant Search, we updated the URL fragment on every new search. If a user came to http://www.indeed.com/resumes and did a search for “java” in Austin, we updated the URL to http://www.indeed.com/resumes#!q=java&l=Austin. For comparison, before we implemented Instant Search, our search URLs used a more traditional query string approach (still supported): http://www.indeed.com/resumes?q=java&l=Austin.

We used #! instead of simply # in order to work with Google’s Making AJAX Applications Crawlable guidelines, which specify a mapping from #! URL fragments to query string parameters so that crawlers can make requests with information stored in the URL fragment.

A drawback of this approach is initial page load performance. Since the data in URL fragments is not sent to the server, the browser has to do a full round-trip (request/response) to the server before processing the fragment, then an additional round-trip to get the search results. In contrast, using the traditional query string approach allows results to come back in a single round-trip.

To improve performance for users coming directly to a search results page from an external site, we used the HTML5 history API supported by most modern browsers. This API enables modifying the whole URL via JavaScript rather than just the URL fragment. We now use this API to update the browser URL to include the search terms in the path (e.g. http://www.indeed.com/resumes/java/in-Austin). These URLs can then be shared and published widely, and users clicking on the resulting links receive search results in a single round-trip.

Templates and Rendering

Dynamically updating search results in the browser with JavaScript provides a superior user experience, but we also need to be able to render identical results on the server. We could have just developed two versions of our templates, one for server-side and one for client-side, but this approach leads to maintainability challenges. It would be annoying and error-prone to keep two versions, in two different template languages, in perfect sync. We could have generated HTML on the server-side only, and send the HTML back in response to the Instant request, but this approach results in a larger response and adds server load. Instead, we opted to use the same template on both the client and server. Technologies like Node.js, mustache, and Google Closure Templates are making this technique viable and more common.

We chose Google Closure Templates to enable a single template that works both on the server (Java-based) and the client (JavaScript-based). Here is an example template:

/**
 * Renders a single block of refinements on the result page
 * @param title Title of the refinement
 * @param refinements List of refinements
 *
 * @private
 */
{template .refinementBlock}
  {if length($refinements) > 0}
    <p class="refine_title">{$title}</p>
    <ul class="refine_by">
    {foreach $refinement in $refinements}
      <li>
        <a class="instl" href="?{$refinement['addParams']}">
          {$refinement['text']}
        </a>
        <span class="refine_count">{$refinement['count']}</span>
      </li>
    {/foreach}
    </ul>
  {/if}
{/template}

The Closure template compiler generates JavaScript that integrates well into the client-side rendering of our search result pages. We also use the templates in our server-side Java code, delivering results quickly for initial requests and enabling subsequent searches for users and crawlers that are not using JavaScript.

No Compromises

Our event-driven architecture has allowed us to build a maintainable library of components that can be used in multiple projects. We have used URL fragments and the HTML5 history API to keep navigation, link sharing, and search engine discovery simple. Closure Templates have helped us avoid unnecessary complexity and duplication. We have built a great resume search experience for our users without compromising on maintainability, reuse, and performance.

At Indeed, we focus on making the job seeker and employer experiences as fast and simple as possible. If these kind of challenges sound interesting to you, check out our open positions.