Tworzenie interfejsu wyszukiwania za pomocą widżetu wyszukiwania

Widżet wyszukiwania udostępnia konfigurowalny interfejs wyszukiwania dla aplikacji internetowych. Do jego wdrożenia potrzeba tylko niewielkiej ilości kodu HTML i JavaScriptu. Umożliwia on stosowanie typowych funkcji wyszukiwania, takich jak filtry i strony podziału. Możesz też dostosowywać części interfejsu za pomocą kodu CSS i JavaScript.

Jeśli potrzebujesz większej elastyczności niż ta, którą zapewnia widżet, rozważ użycie interfejsu Query API. Informacje o tworzeniu interfejsu wyszukiwania za pomocą Query API znajdziesz w artykule Tworzenie interfejsu wyszukiwania za pomocą Query API.

Tworzenie interfejsu wyszukiwania

Aby utworzyć interfejs wyszukiwania, musisz wykonać kilka czynności:

  1. Konfigurowanie wyszukiwarki
  2. wygenerować identyfikator klienta dla aplikacji.
  3. Dodawanie znaczników HTML do pola wyszukiwania i wyników
  4. Wczytywanie widżetu na stronie
  5. Inicjowanie widżetu

Konfigurowanie wyszukiwarki

Każdy interfejs wyszukiwarki musi mieć zdefiniowaną aplikację wyszukiwania w konsoli administracyjnej. Wyszukiwarka udostępnia dodatkowe informacje dotyczące zapytania, takie jak źródła danych, aspekty i ustawienia jakości wyszukiwania.

Aby utworzyć wyszukiwarkę, zapoznaj się z artykułem Utwórz niestandardowe środowisko wyszukiwania.

wygenerować identyfikator klienta dla aplikacji.

Oprócz wykonania czynności opisanych w artykule Konfigurowanie dostępu do interfejsu Google Cloud Search API musisz też wygenerować identyfikator klienta dla aplikacji internetowej.

Konfigurowanie projektu

Podczas konfigurowania projektu:

  • Wybierz typ klienta Przeglądarka internetowa.
  • Podaj identyfikator URI źródła aplikacji.
  • Zapisz utworzony identyfikator klienta. Aby wykonać kolejne czynności, musisz mieć identyfikator klienta. W przypadku widżetu nie jest wymagany tajny klucz klienta.

Więcej informacji znajdziesz w artykule OAuth 2.0 w aplikacji internetowej po stronie klienta.

Dodawanie znaczników HTML

Do działania widżetu wymagana jest niewielka ilość kodu HTML. Musisz podać:

  • Element input w polu wyszukiwania.
  • Element, do którego ma być zakotwiczone wyskakujące okienko z propozycją.
  • Element zawierający wyniki wyszukiwania.
  • (Opcjonalnie) Podaj element, który będzie zawierać elementy sterujące.

Ten fragment kodu HTML pokazuje kod HTML widżetu wyszukiwania, w którym elementy do powiązania są identyfikowane za pomocą atrybutu 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>

Wczytywanie widżetu

Widżet jest wczytywany dynamicznie za pomocą skryptu ładowarki. Aby uwzględnić ładowarkę, użyj tagu <script> w taki sposób:

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>

W tagu skryptu musisz podać wywołanie zwrotne onload. Funkcja jest wywoływana, gdy ładowarka jest gotowa. Gdy ładowarka będzie gotowa, kontynuuj wczytywanie widżetu, wywołując gapi.load(), aby wczytać moduły klienta API, logowania w Google i wyszukiwarki w chmurze.

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)
}

Funkcja initializeApp() jest wywoływana po załadowaniu wszystkich modułów.

Inicjowanie widżetu

Najpierw zainicjuj bibliotekę klienta, wywołując funkcję gapi.client.init() lub gapi.auth2.init() z wygenerowanym identyfikatorem klienta i zakresem https://www.googleapis.com/auth/cloud_search.query. Następnie użyj klas gapi.cloudsearch.widget.resultscontainer.Builder i gapi.cloudsearch.widget.searchbox.Builder, aby skonfigurować widget i powiązać go z elementami HTML.

Poniższy przykład pokazuje, jak zainicjować widżet:

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'
    });
  });
}

W tym przykładzie występują 2 zmienna konfiguracji o tych definicjach:

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/...";

Dostosowywanie logowania

Domyślnie widget prosi użytkowników o zalogowanie się i autoryzowanie aplikacji w momencie, gdy zaczną wpisywać zapytanie. Możesz użyć logowania w Google w witrynach, aby zapewnić użytkownikom bardziej dopasowane logowanie.

Autoryzowanie użytkowników bezpośrednio

Używaj funkcji Zaloguj się przez Google, aby monitorować stan logowania użytkownika i w razie potrzeby logować lub wylogowywać użytkowników. Na przykład w tym przykładzie stan isSignedIn służy do monitorowania zmian dotyczących logowania, a metoda GoogleAuth.signIn() służy do inicjowania logowania po kliknięciu przycisku:

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();
};

Więcej informacji znajdziesz w artykule Logowanie się przez Google.

Automatyczne logowanie użytkowników

Możesz jeszcze bardziej usprawnić proces logowania, autoryzując aplikację z wyprzedzeniem w imieniu użytkowników w Twojej organizacji. Ta metoda jest też przydatna, jeśli do ochrony aplikacji używasz serwera proxy Cloud Identity-Aware.

Więcej informacji znajdziesz w artykule Korzystanie z logowania w Google w aplikacjach IT.

Dostosowywanie interfejsu

Wygląd interfejsu wyszukiwania możesz zmienić, stosując kombinację tych technik:

  • Zastępowanie stylów za pomocą CSS
  • Udekoruj elementy za pomocą adaptera
  • Tworzenie elementów niestandardowych za pomocą adaptera

Zastępowanie stylów za pomocą CSS

Widżet wyszukiwarki ma własny plik CSS do stylizacji elementów sugestii i wyników, a także elementy sterowania stroną. W razie potrzeby możesz zmienić styl tych elementów.

Podczas wczytywania widżet wyszukiwania dynamicznie wczytuje swoją domyślną arkusz stylów. Dzieje się to po załadowaniu arkuszy stylów aplikacji, co zwiększa priorytet reguł. Aby mieć pewność, że Twoje style mają pierwszeństwo przed stylami domyślnymi, używaj selektorów przodków, aby zwiększyć specyficzność reguł domyślnych.

Na przykład poniższa reguła nie ma żadnego wpływu, jeśli jest wczytana w dokumencie w stałym elemencie link lub style.

.cloudsearch_suggestion_container {
  font-size: 14px;
}

Zamiast tego określ regułę za pomocą identyfikatora lub klasy kontenera nadrzędnego zadeklarowanego na stronie.

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

Listę obsługiwanych klas i przykładowy kod HTML utworzony przez widżet znajdziesz w artykule Obsługiwane klasy CSS.

Udekoruj elementy za pomocą adaptera

Aby udekorować element przed renderowaniem, utwórz i zarejestruj adapter, który implementuje jedną z metod dekorowania, np. decorateSuggestionElement lub decorateSearchResultElement..

Na przykład te adaptery dodają klasę niestandardową do elementów sugestii i wyników.

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');
}

Aby zarejestrować adapter podczas inicjowania widżetu, użyj metody setAdapter() klasy Builder:

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();

Dekoratory mogą modyfikować atrybuty elementu kontenera oraz wszystkie elementy podrzędne. Elementy podrzędne można dodawać i usuwać podczas dekorowania. Jeśli jednak wprowadzasz zmiany strukturalne elementów, zamiast dekorowania rozważ utworzenie elementów bezpośrednio.

Tworzenie elementów niestandardowych za pomocą adaptera

Aby utworzyć element niestandardowy dla sugestii, kontenera aspektów lub wyniku wyszukiwania, utwórz i zarejestruj adapter, który implementuje odpowiednio interfejs createSuggestionElement, createFacetResultElement lub createSearchResultElement.

W następujących adapterach pokazano, jak tworzyć niestandardowe elementy sugestii i wyników wyszukiwania za pomocą tagów 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;
}

Aby zarejestrować adapter podczas inicjowania widżetu, użyj metody setAdapter() klasy Builder:

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();

Tworzenie elementów niestandardowych za pomocą createFacetResultElement jest objęte kilkoma ograniczeniami:

  • Musisz dołączyć klasę CSS cloudsearch_facet_bucket_clickable do elementu, który użytkownicy klikają, aby przełączyć grupę.
  • Każdy zbiornik musisz ująć w element zawierający z klasą CSS cloudsearch_facet_bucket_container.
  • Zasobników nie można renderować w innej kolejności niż ta, w której występują w odpowiedzi.

Na przykład poniższy fragment kodu renderuje pola za pomocą linków zamiast pól wyboru.

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) {
  // ...
}

Dostosowywanie zachowania wyszukiwania

Ustawienia aplikacji do wyszukiwania to domyślna konfiguracja interfejsu wyszukiwania i są statyczne. Aby wdrożyć filtry dynamiczne lub aspekty, np. umożliwić użytkownikom przełączanie źródeł danych, możesz zastąpić ustawienia aplikacji wyszukiwarki, przechwytując żądanie wyszukiwania za pomocą adaptera.

Zaimplementuj adapter za pomocą metody interceptSearchRequest, aby modyfikować żądania wysyłane do interfejsu search API przed ich wykonaniem.

Na przykład ten adapter przechwytuje żądania, aby ograniczyć zapytania do źródła wybranego przez użytkownika:

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;
}

Aby zarejestrować adapter podczas inicjowania widżetu, użyj metody setAdapter() podczas kompilowania metody ResultsContainer.

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();

Ten kod HTML służy do wyświetlania pola wyboru do filtrowania według źródeł:

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>

Poniższy kod wykrywa zmianę, ustawia wybór i w razie potrzeby ponownie wykonuje zapytanie.

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);
  }
}

Możesz też przechwycić odpowiedź wyszukiwania, wdrażając interceptSearchResponse w adapterze.

Przypnij wersję interfejsu API

Domyślnie widżet korzysta z najnowszej stabilnej wersji interfejsu API. Aby zablokować konkretną wersję, przed zainicjowaniem widżetu ustaw parametr konfiguracji cloudsearch.config/apiVersion na preferowaną wersję.

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

Jeśli nie zostanie ustawiona lub zostanie ustawiona nieprawidłowa wartość, wersja interfejsu API będzie domyślnie ustawiona na 1.0.

Przypinanie wersji widżetu

Aby uniknąć nieoczekiwanych zmian interfejsów wyszukiwania, ustaw parametr konfiguracji cloudsearch.config/clientVersion w ten sposób:

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

Jeśli wersja widżetu nie zostanie ustawiona lub zostanie ustawiona nieprawidłowa wartość, domyślnie zostanie ustawiona wartość 1.0.

Zabezpieczanie interfejsu wyszukiwania

Wyniki wyszukiwania zawierają szczególnie chronione informacje. Stosuj sprawdzone metody zabezpieczania aplikacji internetowych, zwłaszcza przed atakami clickjacking.

Więcej informacji znajdziesz w projekcie OWASP Guide.

Włączanie debugowania

Użyj interceptSearchRequest, aby włączyć debugowanie widżetu wyszukiwania. Na przykład:

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

  return request;