Tworzenie interfejsu wyszukiwania za pomocą widżetu wyszukiwania

Widżet wyszukiwania udostępnia konfigurowalny interfejs wyszukiwania dla aplikacji internetowych. Widżet wymaga tylko niewielkiej ilości kodu HTML i JavaScript, aby go wdrożyć, i umożliwia korzystanie z popularnych funkcji wyszukiwania, takich jak aspekty i stronicowanie. Możesz też dostosowywać fragmenty interfejsu za pomocą 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ą interfejsu Query API znajdziesz w artykule Tworzenie interfejsu wyszukiwania za pomocą interfejsu Query API.

Tworzenie interfejsu wyszukiwania

Tworzenie interfejsu wyszukiwania wymaga wykonania kilku czynności:

  1. Konfigurowanie wyszukiwarki
  2. Wygeneruj identyfikator klienta 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 wyszukiwania musi mieć zdefiniowaną aplikację wyszukiwania w konsoli administracyjnej. Wyszukiwarka podaje dodatkowe informacje o zapytaniu, takie jak źródła danych, aspekty i ustawienia jakości wyszukiwania.

Aby utworzyć wyszukiwarkę, zapoznaj się z artykułem Tworzenie niestandardowego środowiska wyszukiwania.

Wygeneruj identyfikator klienta aplikacji.

Oprócz 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.
  • Podaj identyfikator URI źródła aplikacji.
  • Zapisz utworzony identyfikator klienta. Aby wykonać następne kroki, musisz mieć identyfikator klienta. W przypadku widżetu tajny klucz klienta nie jest wymagany.

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

Dodawanie znaczników HTML

Aby widżet działał, potrzebuje niewielkiej ilości kodu HTML. Musisz podać:

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

Poniższy fragment kodu HTML przedstawia 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 ładowany dynamicznie za pomocą skryptu wczytującego. Aby uwzględnić moduł wczytujący, użyj tagu <script> w ten 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 moduł wczytujący jest gotowy. Gdy moduł wczytujący będzie gotowy, wczytaj widżet, wywołując funkcję gapi.load(), aby wczytać klienta interfejsu API, logowanie przez Google i moduły 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)
}

Funkcja initializeApp() jest wywoływana po wczytaniu 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.Buildergapi.cloudsearch.widget.searchbox.Builder, aby skonfigurować widżet 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 powyższym przykładzie odwołujemy się do 2 zmiennych konfiguracji zdefiniowanych w ten sposób:

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 procesu logowania

Domyślnie widżet prosi użytkowników o zalogowanie się i autoryzację aplikacji w momencie, gdy zaczynają wpisywać zapytanie. Możesz użyć logowania przez Google w witrynach, aby zapewnić użytkownikom bardziej spersonalizowane logowanie.

Bezpośrednie autoryzowanie użytkowników

Użyj Zaloguj się przez Google, aby monitorować stan logowania użytkownika i w razie potrzeby logować lub wylogowywać użytkowników. Na przykład poniższy przykład obserwuje stan isSignedIn, aby monitorować zmiany logowania, i używa metody GoogleAuth.signIn(), aby zainicjować logowanie 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 uprościć proces logowania, wstępnie autoryzując aplikację w imieniu użytkowników w Twojej organizacji. Ta technika jest przydatna również w przypadku korzystania z Cloud Identity Aware Proxy do ochrony aplikacji.

Więcej informacji znajdziesz w artykule Używanie logowania przez Google w aplikacjach IT.

Dostosowywanie interfejsu

Wygląd interfejsu wyszukiwania możesz zmienić za pomocą kombinacji 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 wyszukiwania ma własny arkusz CSS, który umożliwia stylizowanie elementów sugestii i wyników, a także elementów sterujących paginacją. W razie potrzeby możesz zmienić styl tych elementów.

Podczas wczytywania widżet wyszukiwania dynamicznie wczytuje domyślny 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żyj selektorów elementów nadrzędnych, aby zwiększyć szczegółowość reguł domyślnych.

Na przykład poniższa reguła nie działa, jeśli jest wczytywana w statycznym tagu link lub style w dokumencie.

.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 generowany przez widżet znajdziesz w sekcji 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()Builder klasy:

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, a także wszystkie elementy podrzędne. Podczas dekorowania można dodawać i usuwać elementy podrzędne. Jeśli jednak wprowadzasz zmiany strukturalne w elementach, rozważ utworzenie ich bezpośrednio zamiast dekorowania.

Tworzenie elementów niestandardowych za pomocą adaptera

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

Poniższe adaptery pokazują, 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 niestandardowych elementów aspektów za pomocą createFacetResultElement podlega kilku ograniczeniom:

  • Musisz dołączyć klasę CSS cloudsearch_facet_bucket_clickable do elementu, który użytkownicy klikają, aby przełączać zasobnik.
  • Każdy koszyk musi być umieszczony w elemencie zawierającym z klasą CSS cloudsearch_facet_bucket_container.
  • Nie możesz renderować przedziałów w innej kolejności niż w odpowiedzi.

Na przykład ten fragment kodu renderuje aspekty 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 działania wyszukiwarki

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

Zaimplementuj adapter z metodą interceptSearchRequest do modyfikowania żądań wysyłanych 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 tworzenia 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();

Poniższy kod HTML służy do wyświetlania pola wyboru, które umożliwia filtrowanie 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 nasłuchuje zmiany, 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, implementując w adapterze funkcję interceptSearchResponse.

Przypinanie wersji interfejsu API

Domyślnie widżet używa 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 wersja interfejsu API nie zostanie ustawiona lub będzie miała nieprawidłową wartość, domyślnie będzie to 1.0.

Przypinanie wersji widżetu

Aby uniknąć nieoczekiwanych zmian w interfejsach 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 będzie miała nieprawidłową wartość, domyślnie będzie to 1.0.

Zabezpieczanie interfejsu wyszukiwania

Wyniki wyszukiwania zawierają informacje o charakterze poufnym. Postępuj zgodnie ze sprawdzonymi metodami zabezpieczania aplikacji internetowych, zwłaszcza przed atakami typu clickjacking.

Więcej informacji znajdziesz w przewodniku OWASP.

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;