Tworzenie interfejsu wyszukiwania za pomocą widżetu wyszukiwania

Widżet wyszukiwania udostępnia dostosowywany interfejs wyszukiwania dla aplikacji internetowych. Widżet wymaga tylko niewielkiej ilości kodu HTML i JavaScriptu do zaimplementowania i umożliwia korzystanie z popularnych funkcji wyszukiwania, takich jak aspekty i podział na strony. Możesz też dostosowywać fragmenty interfejsu za pomocą CSS i JavaScriptu.

Jeśli potrzebujesz większej elastyczności niż 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.

Utwórz interfejs wyszukiwania

Utworzenie interfejsu wyszukiwania składa się z kilku kroków:

  1. Konfigurowanie wyszukiwarki
  2. Wygeneruj identyfikator klienta dla aplikacji
  3. Dodaj znaczniki HTML pola wyszukiwania i wyników
  4. Wczytaj widżet na stronie
  5. Inicjowanie widżetu

Konfigurowanie wyszukiwarki

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

Aby dowiedzieć się, jak utworzyć wyszukiwarkę, zapoznaj się z informacjami o tworzeniu niestandardowego środowiska wyszukiwania.

Wygeneruj identyfikator klienta dla aplikacji

Oprócz czynności opisanych w sekcji 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. Do wykonania kolejnych kroków będzie Ci potrzebny identyfikator klienta. Widżet nie wymaga tajnego klucza klienta.

Więcej informacji znajdziesz w artykule na temat protokołu OAuth 2.0 dla aplikacji internetowej po stronie klienta.

Dodaj znaczniki HTML

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

  • Element input pola wyszukiwania.
  • Element, do którego będzie zakotwiczone wyskakujące okienko sugestii.
  • Element zawierający wyniki wyszukiwania.
  • (Opcjonalnie) Podaj element, który będzie zawierał elementy sterujące aspektów.

Poniższy fragment kodu HTML zawiera kod HTML widżetu wyszukiwania. Elementy, które mają być powiązane, są oznaczone atrybutem 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 przez skrypt wczytujący. Aby uwzględnić moduł wczytywania, użyj tagu <script> w podany niżej 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 program wczytywania będzie gotowy. Gdy moduł wczytywania będzie gotowy, kontynuuj wczytywanie widżetu, wywołując metodę gapi.load(), aby wczytać klienta interfejsu API, Logowanie przez Google i 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 załadowaniu wszystkich modułów.

Inicjowanie widżetu

Najpierw zainicjuj bibliotekę klienta, wywołując metodę 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 do skonfigurowania widżetu i powiązania 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'
    });
  });
}

Powyższy przykład odwołuje się do 2 zmiennych na potrzeby konfiguracji zdefiniowanej jako:

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 rozpoczynają wpisywanie zapytania. Możesz używać Logowania przez Google w witrynach, aby zapewnić użytkownikom większą wygodę logowania.

Bezpośrednie autoryzowanie użytkowników

Użyj funkcji Zaloguj się przez Google, aby monitorować stan logowania użytkownika oraz logować się lub wylogowywać użytkowników w razie potrzeby. Na przykład w tym przykładzie zaobserwowano stan isSignedIn służący do monitorowania zmian w logowaniu się oraz wykorzystuje metodę GoogleAuth.signIn() 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 przez Google.

Automatycznie loguj użytkowników

Możesz jeszcze bardziej uprościć logowanie, autoryzując aplikację w imieniu użytkowników w organizacji. Ta metoda jest też przydatna, gdy używasz Cloud Identity Aware Proxy do ochrony aplikacji.

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

Dostosowywanie interfejsu

Wygląd interfejsu wyszukiwania możesz zmienić, korzystając z różnych metod:

  • Zastąp style za pomocą CSS
  • Udekoruj elementy adapterem
  • Tworzenie elementów niestandardowych za pomocą adaptera

Zastąp style za pomocą CSS

Widżet wyszukiwania zawiera własny styl CSS do określania stylu sugestii i elementów wyników, a także elementy sterujące podziału na strony. W razie potrzeby możesz zmienić ich styl.

Podczas wczytywania widżet wyszukiwania dynamicznie wczytuje swój domyślny arkusz stylów. Dzieje się tak po wczytaniu arkuszy stylów aplikacji, co zwiększa priorytet reguł. Aby mieć pewność, że Twoje własne style mają pierwszeństwo przed stylami domyślnymi, użyj selektorów elementów nadrzędnych, aby zwiększyć specyfikę reguł domyślnych.

Na przykład ta reguła nie będzie działać, jeśli zostanie załadowana w statycznym tagu link lub style w dokumencie.

.cloudsearch_suggestion_container {
  font-size: 14px;
}

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

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

Listę klas pomocy i przykładowy kod HTML wygenerowany przez widżet znajdziesz w dokumentacji obsługiwanych klas CSS.

Udekoruj elementy adapterem

Aby udekorować element przed renderowaniem, utwórz i ponownie użyj adaptera, który stosuje jedną z metod dekorowania, np. decorateSuggestionElement lub decorateSearchResultElement..

Na przykład poniższe 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() odpowiedniej 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();

Dekoracje mogą zmieniać atrybuty elementu kontenera oraz elementów podrzędnych. Elementy podrzędne można dodawać lub usuwać podczas dekoracji. Jeśli jednak wprowadzasz zmiany strukturalne elementów, rozważ utworzenie ich bezpośrednio, a nie dekorowanie.

Tworzenie elementów niestandardowych za pomocą adaptera

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

Poniższe adaptery pokazują tworzenie niestandardowych elementów 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() odpowiedniej 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 klikają użytkownicy, aby przełączyć zasobnik.
  • Każdy zasobnik musisz umieścić w elemencie zawierającym klasę CSS cloudsearch_facet_bucket_container.
  • Nie można wyrenderować zasobników w innej kolejności niż występuje 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 wyszukiwania

Ustawienia wyszukiwarki reprezentują domyślną konfigurację 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 wyszukiwarki przez przechwycenie żądania wyszukiwania za pomocą adaptera.

Zaimplementuj adapter z metodą interceptSearchRequest, aby modyfikować żądania wysyłane do interfejsu Search API przed wykonaniem.

Na przykład ten adapter przechwytuje żądania ograniczenia zapytań 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 komponentu 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>

Podany niżej kod nasłuchuje zmiany, ustawia wybór i w razie potrzeby wykonuje zapytanie ponownie.

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ź na wyszukiwanie, implementując w adapterze interceptSearchResponse.

Przypnij wersję 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 jest nieskonfigurowana lub ustawiona na nieprawidłową wartość, domyślnie będzie to 1.0.

Przypnij wersję widżetu

Aby uniknąć nieoczekiwanych zmian w interfejsach wyszukiwania, ustaw parametr konfiguracji cloudsearch.config/clientVersion w podany niżej sposób:

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

Jeśli wersja widżetu jest nieskonfigurowana lub ustawiona jest nieprawidłowa wartość, domyślnie będzie to 1.0.

Zabezpieczanie interfejsu wyszukiwania

Wyniki wyszukiwania zawierają informacje poufne. Postępuj zgodnie ze sprawdzonymi metodami zabezpieczania aplikacji internetowych, w szczególności przed atakami typu clickjacking.

Więcej informacji znajdziesz w dokumencie OWASP Guide Project

Włącz debugowanie

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;