検索ウィジェットを使用して検索インターフェースを作成する

コレクションでコンテンツを整理 必要に応じて、コンテンツの保存と分類を行います。

検索ウィジェットは、ウェブ アプリケーション向けにカスタマイズ可能な検索インターフェースを提供します。このウィジェットでは、ファセットやページネーションなどの一般的な検索機能を実装し、少量の HTML と JavaScript のみを実装できます。CSS や JavaScript を使用してインターフェースの一部をカスタマイズすることもできます。

ウィジェットで提供される以上の柔軟性が必要な場合は、Query API の使用を検討してください。Query API を使用した検索インターフェースの作成については、Query API を使用した検索インターフェースの作成をご覧ください。

検索インターフェースを作成する

検索インターフェースを作成するには、いくつかの手順が必要です。

  1. 検索アプリケーションを構成する
  2. アプリケーションのクライアント ID を生成する
  3. 検索ボックスと結果用の HTML マークアップを追加する
  4. ページにウィジェットを読み込む
  5. ウィジェットを初期化する

検索アプリケーションを構成する

各検索インターフェースには、管理コンソールで定義された検索アプリケーションが必要です。検索アプリケーションは、データソース、ファセット、検索品質設定など、クエリに関する追加情報を提供します。

検索アプリケーションを作成するには、カスタム検索エクスペリエンスを作成するをご覧ください。

アプリケーションのクライアント ID を生成する

Google Cloud Search REST API へのアクセスを構成するの手順に加えて、ウェブ アプリケーションのクライアント ID も生成する必要があります。

プロジェクトを構成する

プロジェクトを構成するには:

  • ウェブブラウザのクライアント タイプを選択します。
  • アプリの生成元 URI を指定します。
  • 作成されたクライアント ID をメモします。次の手順を完了するには、クライアント ID が必要です。ウィジェットにはクライアント シークレットは必要ありません。

詳細については、クライアント側ウェブ アプリケーションの OAuth 2.0 をご覧ください。

HTML マークアップを追加する

ウィジェットが機能するには、少量の HTML が必要です。次の情報を指定する必要があります。

  • 検索ボックスの input 要素。
  • 候補ポップアップを固定するための要素。
  • 検索結果を格納する要素。
  • (省略可)ファセット コントロールを格納する要素を指定します。

次の HTML スニペットは、検索ウィジェットの HTML を示しており、バインドされる要素は id 属性で識別されます。

serving/widget/public/with_css/index.html
<div id="search_bar">
  <div id="suggestions_anchor">
    <input type="text" id="search_input" placeholder="Search for...">
  </div>
</div>
<div id="facet_results"></div>
<div id="search_results"></div>

ウィジェットを読み込む

ウィジェットは、ローダー スクリプトを介して動的に読み込まれます。ローダを含めるには、次のように <script> タグを使用します。

serving/widget/public/with_css/index.html
<!-- Google API loader -->
<script src="https://apis.google.com/js/api.js?mods=enable_cloud_search_widget&onload=onLoad" async defer></script>

スクリプトタグで onload コールバックを指定する必要があります。この関数は、ローダーの準備ができると呼び出されます。ローダの準備ができたら、引き続き gapi.load() を呼び出してウィジェットを読み込み、API クライアント、Google ログイン、Cloud Search モジュールを読み込みます。

serving/widget/public/with_css/app.js
/**
* Load the cloud search widget & auth libraries. Runs after
* the initial gapi bootstrap library is ready.
*/
function onLoad() {
  gapi.load('client:auth2:cloudsearch-widget', initializeApp)
}

initializeApp() 関数は、すべてのモジュールが読み込まれた後に呼び出されます。

ウィジェットを初期化する

まず、生成されたクライアント ID と https://www.googleapis.com/auth/cloud_search.query スコープを使用して gapi.client.init() または gapi.auth2.init() を呼び出し、クライアント ライブラリを初期化します。次に、gapi.cloudsearch.widget.resultscontainer.Builder クラスと gapi.cloudsearch.widget.searchbox.Builder クラスを使用してウィジェットを構成し、HTML 要素にバインドします。

次の例は、ウィジェットを初期化する方法を示しています。

serving/widget/public/with_css/app.js
/**
 * Initialize the app after loading the Google API client &
 * Cloud Search widget.
 */
function initializeApp() {
  // Load client ID & search app.
  loadConfiguration().then(function() {
    // Set API version to v1.
    gapi.config.update('cloudsearch.config/apiVersion', 'v1');

    // Build the result container and bind to DOM elements.
    var resultsContainer = new gapi.cloudsearch.widget.resultscontainer.Builder()
      .setSearchApplicationId(searchApplicationName)
      .setSearchResultsContainerElement(document.getElementById('search_results'))
      .setFacetResultsContainerElement(document.getElementById('facet_results'))
      .build();

    // Build the search box and bind to DOM elements.
    var searchBox = new gapi.cloudsearch.widget.searchbox.Builder()
      .setSearchApplicationId(searchApplicationName)
      .setInput(document.getElementById('search_input'))
      .setAnchor(document.getElementById('suggestions_anchor'))
      .setResultsContainer(resultsContainer)
      .build();
  }).then(function() {
    // Init API/oauth client w/client ID.
    return gapi.auth2.init({
        'clientId': clientId,
        'scope': 'https://www.googleapis.com/auth/cloud_search.query'
    });
  });
}

上記の例では、次のように定義された 2 つの変数を構成のために参照しています。

serving/widget/public/with_css/app.js
/**
* Client ID from OAuth credentials.
*/
var clientId = "...apps.googleusercontent.com";

/**
* Full resource name of the search application, such as
* "searchapplications/<your-id>".
*/
var searchApplicationName = "searchapplications/...";

ログイン エクスペリエンスをカスタマイズする

デフォルトでは、ウィジェットはユーザーがクエリの入力を開始した際に、ログインしてアプリを承認するように求めます。ウェブサイト用の Google ログインを使用すると、ユーザーに合わせてカスタマイズしたログイン方式を提供できます。

ユーザーを直接承認する

Google でログインを使用してユーザーのログイン状態をモニタリングし、必要に応じてユーザーをログインまたはログアウトします。たとえば、次の例では、isSignedIn 状態をモニタリングしてログインの変更をモニタリングし、GoogleAuth.signIn() メソッドを使用してボタンのクリックでログインを開始します。

serving/widget/public/with_signin/app.js
// Handle sign-in/sign-out.
let auth = gapi.auth2.getAuthInstance();

// Watch for sign in status changes to update the UI appropriately.
let onSignInChanged = (isSignedIn) => {
  // Update UI to switch between signed in/out states
  // ...
}
auth.isSignedIn.listen(onSignInChanged);
onSignInChanged(auth.isSignedIn.get()); // Trigger with current status.

// Connect sign-in/sign-out buttons.
document.getElementById("sign-in").onclick = function(e) {
  auth.signIn();
};
document.getElementById("sign-out").onclick = function(e) {
  auth.signOut();
};

詳しくは、Google でログインをご覧ください。

ユーザーを自動的にログインさせる

組織内のユーザーに代わってアプリケーションを事前に承認することで、ログイン方式をさらに簡素化できます。この手法は、Cloud Identity Aware Proxy を使用してアプリケーションを保護する場合にも役立ちます。

詳細については、IT アプリで Google ログインを使用するをご覧ください。

インターフェースをカスタマイズする

検索インターフェースの外観は、複数の手法を組み合わせて変更できます。

  • CSS でスタイルをオーバーライドする
  • 要素をアダプタで修飾する
  • アダプタを使用してカスタム要素を作成する

CSS でスタイルをオーバーライドする

検索ウィジェットには、候補と結果の要素をスタイル設定する独自の CSS と、ページ分割のコントロールが用意されています。これらの要素は必要に応じてスタイルを変更できます。

読み込み時に、検索ウィジェットはデフォルトのスタイルシートを動的に読み込みます。この処理は、アプリケーションのスタイルシートが読み込まれた後に行われ、ルールの優先度が上がります。独自のスタイルをデフォルト スタイルよりも優先させるには、祖先セレクタを使用してデフォルト ルールの特異性を高めます。

たとえば、ドキュメント内の静的 link タグまたは style タグに読み込まれる場合、次のルールは無効です。

.cloudsearch_suggestion_container {
  font-size: 14px;
}

代わりに、ページで宣言されている祖先コンテナの ID またはクラスでルールを修飾します。

#suggestions_anchor .cloudsearch_suggestion_container {
  font-size: 14px;
}

サポートクラスのリストとウィジェットによって生成される HTML の例については、サポートされる CSS クラス リファレンスをご覧ください。

要素をアダプタで修飾する

レンダリング前に要素を装飾するには、decorateSuggestionElementdecorateSearchResultElement. などの装飾方法のいずれかを実装するアダプタを作成して登録します。

たとえば、次のアダプタは、候補要素と結果要素にカスタムクラスを追加します。

serving/widget/public/with_decorated_element/app.js
/**
 * Search box adapter that decorates suggestion elements by
 * adding a custom CSS class.
 */
function SearchBoxAdapter() {}
SearchBoxAdapter.prototype.decorateSuggestionElement = function(element) {
  element.classList.add('my-suggestion');
}

/**
 * Results container adapter that decorates suggestion elements by
 * adding a custom CSS class.
 */
function ResultsContainerAdapter() {}
ResultsContainerAdapter.prototype.decorateSearchResultElement = function(element) {
  element.classList.add('my-result');
}

ウィジェットを初期化するときにアダプタを登録するには、それぞれの Builder クラスの setAdapter() メソッドを使用します。

serving/widget/public/with_decorated_element/app.js
// Build the result container and bind to DOM elements.
var resultsContainer = new gapi.cloudsearch.widget.resultscontainer.Builder()
  .setAdapter(new ResultsContainerAdapter())
  // ...
  .build();

// Build the search box and bind to DOM elements.
var searchBox = new gapi.cloudsearch.widget.searchbox.Builder()
  .setAdapter(new SearchBoxAdapter())
  // ...
  .build();

デコレータは、コンテナ要素といずれかの子要素の属性を変更できます。子要素は、装飾時に追加または削除できます。ただし、要素に構造的な変更を行う場合は、装飾するのではなく、要素を直接作成することを検討してください。

アダプタを使用してカスタム要素を作成する

候補、ファセット コンテナ、検索結果のカスタム要素を作成するには、createSuggestionElementcreateFacetResultElementcreateSearchResultElement をそれぞれ実装するアダプタを作成して登録します。

次のアダプターは、HTML <template> タグを使用してカスタム候補と検索結果要素を作成する方法を示しています。

serving/widget/public/with_custom_element/app.js
/**
 * Search box adapter that overrides creation of suggestion elements.
 */
function SearchBoxAdapter() {}
SearchBoxAdapter.prototype.createSuggestionElement = function(suggestion) {
  let template = document.querySelector('#suggestion_template');
  let fragment = document.importNode(template.content, true);
  fragment.querySelector('.suggested_query').textContent = suggestion.suggestedQuery;
  return fragment.firstElementChild;
}

/**
 * Results container adapter that overrides creation of result elements.
 */
function ResultsContainerAdapter() {}
ResultsContainerAdapter.prototype.createSearchResultElement = function(result) {
  let template = document.querySelector('#result_template');
  let fragment = document.importNode(template.content, true);
  fragment.querySelector('.title').textContent = result.title;
  fragment.querySelector('.title').href = result.url;
  let snippetText = result.snippet != null ?
    result.snippet.snippet : '';
  fragment.querySelector('.query_snippet').innerHTML = snippetText;
  return fragment.firstElementChild;
}

ウィジェットを初期化するときにアダプタを登録するには、それぞれの Builder クラスの setAdapter() メソッドを使用します。

serving/widget/public/with_custom_element/app.js
// Build the result container and bind to DOM elements.
var resultsContainer = new gapi.cloudsearch.widget.resultscontainer.Builder()
  .setAdapter(new ResultsContainerAdapter())
  // ...
  .build();

// Build the search box and bind to DOM elements.
var searchBox = new gapi.cloudsearch.widget.searchbox.Builder()
  .setAdapter(new SearchBoxAdapter())
  // ...
  .build();

createFacetResultElement を使用したカスタム ファセット要素の作成には、いくつかの制限があります。

  • CSS クラス cloudsearch_facet_bucket_clickable を、ユーザーがバケットを切り替えてクリックする要素に添付する必要があります。
  • 各要素を包含する要素内で CSS クラス cloudsearch_facet_bucket_container でラップする必要があります。
  • バケットをレスポンスに表示される順序と異なる順序でレンダリングすることはできません。

たとえば、次のスニペットは、チェックボックスの代わりにリンクを使用してファセットをレンダリングします。

serving/widget/public/with_custom_facet/app.js
/**
 * Results container adapter that intercepts requests to dynamically
 * change which sources are enabled based on user selection.
 */
function ResultsContainerAdapter() {
  this.selectedSource = null;
}

ResultsContainerAdapter.prototype.createFacetResultElement = function(result) {
  // container for the facet
  var container = document.createElement('div');

  // Add a label describing the facet (operator/property)
  var label = document.createElement('div')
  label.classList.add('facet_label');
  label.textContent = result.operatorName;
  container.appendChild(label);

  // Add each bucket
  for(var i in result.buckets) {
    var bucket = document.createElement('div');
    bucket.classList.add('cloudsearch_facet_bucket_container');

    // Extract & render value from structured value
    // Note: implementation of renderValue() not shown
    var bucketValue = this.renderValue(result.buckets[i].value)
    var link = document.createElement('a');
    link.classList.add('cloudsearch_facet_bucket_clickable');
    link.textContent = bucketValue;
    bucket.appendChild(link);
    container.appendChild(bucket);
  }
  return container;
}

// Renders a value for user display
ResultsContainerAdapter.prototype.renderValue = function(value) {
  // ...
}

検索動作をカスタマイズする

検索アプリケーションの設定は、検索インターフェースのデフォルト構成を表し、静的です。ユーザーにデータソースの切り替えを許可するような動的なフィルタやファセットを実装する場合、アダプタで検索リクエストをインターセプトすることにより、検索アプリケーションの設定をオーバーライドできます。

実行前に Search API に対するリクエストを変更するには、interceptSearchRequest メソッドでアダプタを実装します。

たとえば、次のアダプタはリクエストをインターセプトして、クエリをユーザーが選択したソースに限定します。

serving/widget/public/with_request_interceptor/app.js
/**
 * Results container adapter that intercepts requests to dynamically
 * change which sources are enabled based on user selection.
 */
function ResultsContainerAdapter() {
  this.selectedSource = null;
}
ResultsContainerAdapter.prototype.interceptSearchRequest = function(request) {
  if (!this.selectedSource || this.selectedSource == 'ALL') {
    // Everything selected, fall back to sources defined in the search
    // application.
    request.dataSourceRestrictions = null;
  } else {
    // Restrict to a single selected source.
    request.dataSourceRestrictions = [
      {
        source: {
          predefinedSource: this.selectedSource
        }
      }
    ];
  }
  return request;
}

ウィジェットの初期化時にアダプタを登録するには、ResultsContainer のビルド時に setAdapter() メソッドを使用します。

serving/widget/public/with_request_interceptor/app.js
var resultsContainerAdapter = new ResultsContainerAdapter();
// Build the result container and bind to DOM elements.
var resultsContainer = new gapi.cloudsearch.widget.resultscontainer.Builder()
  .setAdapter(resultsContainerAdapter)
  // ...
  .build();

次の HTML は、ソースでフィルタリングするために選択ボックスを表示する目的で使用されます。

serving/widget/public/with_request_interceptor/index.html
<div>
  <span>Source</span>
  <select id="sources">
    <option value="ALL">All</option>
    <option value="GOOGLE_GMAIL">Gmail</option>
    <option value="GOOGLE_DRIVE">Drive</option>
    <option value="GOOGLE_SITES">Sites</option>
    <option value="GOOGLE_GROUPS">Groups</option>
    <option value="GOOGLE_CALENDAR">Calendar</option>
    <option value="GOOGLE_KEEP">Keep</option>
  </select>
</div>

次のコードは、変更をリッスンし、選択を設定し、必要に応じてクエリを再実行します。

serving/widget/public/with_request_interceptor/app.js
// Handle source selection
document.getElementById('sources').onchange = (e) => {
  resultsContainerAdapter.selectedSource = e.target.value;
  let request = resultsContainer.getCurrentRequest();
  if (request.query) {
    // Re-execute if there's a valid query. The source selection
    // will be applied in the interceptor.
    resultsContainer.resetState();
    resultsContainer.executeRequest(request);
  }
}

アダプターで interceptSearchResponse を実装して、検索レスポンスをインターセプトすることもできます。

API バージョンを固定する

ウィジェットでは、デフォルトで最新の安定版 API が使用されます。特定のバージョンにロックインするには、ウィジェットを初期化する前に cloudsearch.config/apiVersion 構成パラメータを目的のバージョンに設定します。

serving/widget/public/basic/app.js
gapi.config.update('cloudsearch.config/apiVersion', 'v1');

API のバージョンは、省略するか無効な値を指定した場合、デフォルトの 1.0 になります。

ウィジェットのバージョンを固定する

検索インターフェースの予期しない変更を回避するには、次のように cloudsearch.config/clientVersion 構成パラメータを設定します。

gapi.config.update('cloudsearch.config/clientVersion', 1.1);

ウィジェットのバージョンは、省略するか無効な値を指定した場合、デフォルトの 1.0 になります。

検索インターフェースを保護する

検索結果には非常に機密性の高い情報が含まれます。おすすめの方法に沿ってウェブ アプリケーションを(特にクリックジャッキング攻撃に対して)保護します。

詳細については、OWASP Guide Project をご覧ください。

デバッグを有効にする

interceptSearchRequest を使用して、検索ウィジェットのデバッグを有効にします。次に例を示します。

  if (!request.requestOptions) {
  // Make sure requestOptions is populated
  request.requestOptions = {};
  }
  // Enable debugging
  request.requestOptions.debugOptions = {enableDebugging: true}

  return request;