יצירה של ממשק חיפוש באמצעות ווידג'ט החיפוש

ווידג'ט החיפוש מספק ממשק חיפוש שאפשר להתאים אישית לאפליקציות אינטרנט. הווידג'ט דורש רק כמות קטנה של HTML ו-JavaScript להטמעה, ומאפשר שימוש בתכונות חיפוש נפוצות כמו פילוח והחלפה בין דפים. אפשר גם להתאים אישית חלקים מהממשק באמצעות CSS ו-JavaScript.

אם אתם צריכים גמישות רבה יותר מזו שמוצעת בווידג'ט, כדאי להשתמש ב-Query API. מידע על יצירת ממשק חיפוש באמצעות Query API מופיע במאמר יצירת ממשק חיפוש באמצעות Query API.

בניית ממשק חיפוש

כדי ליצור את ממשק החיפוש, צריך לבצע כמה שלבים:

  1. הגדרת אפליקציית חיפוש
  2. יצירת מזהה לקוח לאפליקציה
  3. הוספת תגי עיצוב HTML לתיבת החיפוש ולתוצאות
  4. טעינת הווידג'ט בדף
  5. הפעלת הווידג'ט

הגדרת אפליקציית חיפוש

לכל ממשק חיפוש צריך להיות מוגדר יישום חיפוש במסוף Admin. אפליקציית החיפוש מספקת מידע נוסף על השאילתה, כמו מקורות הנתונים, ההיבטים והגדרות איכות החיפוש.

כדי ליצור אפליקציית חיפוש, אפשר לעיין במאמר יצירת חוויית חיפוש בהתאמה אישית.

יצירת מזהה לקוח לאפליקציה

בנוסף לשלבים שמפורטים במאמר הגדרת גישה ל-Google Cloud Search API, צריך גם ליצור מזהה לקוח לאפליקציית האינטרנט.

הגדרת פרויקט

כשמגדירים את הפרויקט:

  • בוחרים את סוג הלקוח דפדפן אינטרנט.
  • מציינים את URI המקור של האפליקציה.
  • רושמים את מזהה הלקוח שנוצר. תצטרכו את מזהה הלקוח כדי להשלים את השלבים הבאים. אין צורך בסוד לקוח עבור הווידג'ט.

מידע נוסף זמין במאמר בנושא 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 callback בתג script. הפונקציה נקראת כשהטוען מוכן. כשהטוען מוכן, ממשיכים לטעון את הווידג'ט על ידי קריאה ל-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() אחרי שכל המודולים נטענים.

הפעלת הווידג'ט

קודם, מאתחלים את ספריית הלקוח על ידי קריאה ל-gapi.client.init() או ל-gapi.auth2.init() עם מזהה הלקוח שנוצר וההיקף https://www.googleapis.com/auth/cloud_search.query. לאחר מכן, משתמשים במחלקות 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'
    });
  });
}

בדוגמה שלמעלה יש הפניה לשני משתנים להגדרה שמוגדרים כך:

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.

כניסה אוטומטית של משתמשים

כדי לייעל עוד יותר את חוויית הכניסה, אפשר לתת מראש הרשאה לאפליקציה בשם המשתמשים בארגון. הטכניקה הזו שימושית גם אם משתמשים בשרת Proxy לאימות זהויות (IAP) ב-Cloud Identity כדי להגן על האפליקציה.

מידע נוסף זמין במאמר שימוש בכניסה לחשבון Google עם אפליקציות IT.

התאמה אישית של הממשק

אפשר לשנות את המראה של ממשק החיפוש באמצעות שילוב של טכניקות:

  • שינוי הסגנונות באמצעות CSS
  • קישוט הרכיבים באמצעות מתאם
  • יצירת רכיבים מותאמים אישית באמצעות מתאם

שינוי הסגנונות באמצעות CSS

לווידג'ט של החיפוש יש CSS משלו לעיצוב של רכיבי ההצעות והתוצאות, וגם של אמצעי הניווט בין הדפים. אפשר לשנות את הסגנון של הרכיבים האלה לפי הצורך.

במהלך הטעינה, ווידג'ט החיפוש טוען באופן דינמי את גיליון הסגנונות שמוגדר כברירת מחדל. הפעולה הזו מתבצעת אחרי טעינת גיליונות הסגנון של האפליקציה, וכך עולה העדיפות של הכללים. כדי לוודא שהסגנונות שלכם יקבלו עדיפות על פני סגנונות ברירת המחדל, צריך להשתמש בבוררי צאצא כדי להגדיל את הספציפיות של כללי ברירת המחדל.

לדוגמה, לכלל הבא אין השפעה אם הוא נטען בתג סטטי של link או style במסמך.

.cloudsearch_suggestion_container {
  font-size: 14px;
}

במקום זאת, צריך להגדיר את הכלל באמצעות המזהה או המחלקה של מאגר התגים ברמת העל שהוגדר בדף.

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

במאמר בנושא מחלקות CSS נתמכות מופיעה רשימה של מחלקות תמיכה ודוגמה לקוד HTML שנוצר על ידי הווידג'ט.

קישוט הרכיבים באמצעות מתאם

כדי להוסיף מאפיינים לאלמנט לפני העיבוד שלו, צריך ליצור ולרשום מתאם שמטמיע אחת משיטות ההוספה, כמו decorateSuggestionElement או decorateSearchResultElement..

לדוגמה, המתאמים הבאים מוסיפים מחלקה מותאמת אישית לרכיבי ההצעה והתוצאה.

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

כדי לרשום את המתאם כשמפעילים את הווידג'ט, משתמשים בשיטה setAdapter() של המחלקה 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();

המאפיינים של אלמנט הקונטיינר ושל כל אלמנט צאצא יכולים להשתנות על ידי תגי העיצוב. אפשר להוסיף או להסיר רכיבי צאצא במהלך העיצוב. עם זאת, אם מבצעים שינויים מבניים ברכיבים, כדאי ליצור את הרכיבים ישירות במקום להשתמש בשיטת ה-decorator.

יצירת רכיבים מותאמים אישית באמצעות מתאם

כדי ליצור רכיב בהתאמה אישית להצעה, למאגר פנים או לתוצאת חיפוש, יוצרים מתאם ומבצעים רישום שלו שמטמיע את createSuggestionElement,‏ createFacetResultElement או createSearchResultElement בהתאמה.

בדוגמאות הבאות מוצגים מתאמים שיוצרים רכיבים מותאמים אישית של הצעות ותוצאות חיפוש באמצעות תגי 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;
}

כדי לרשום את המתאם כשמפעילים את הווידג'ט, משתמשים בשיטה setAdapter() של המחלקה 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();

יצירת רכיבי היבט בהתאמה אישית באמצעות 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) {
  // ...
}

התאמה אישית של אופן הפעולה של החיפוש

ההגדרות של אפליקציית החיפוש מייצגות את הגדרות ברירת המחדל של ממשק החיפוש, והן קבועות. כדי להטמיע מסננים או היבטים דינמיים, כמו אפשרות למשתמשים להחליף בין מקורות נתונים, אפשר לבטל את ההגדרות של אפליקציית החיפוש על ידי יירוט של בקשת החיפוש באמצעות מתאם.

מטמיעים מתאם עם השיטה interceptSearchRequest כדי לשנות בקשות שנשלחות אל Search API לפני ההפעלה.

לדוגמה, המתאם הבא מיירט בקשות כדי להגביל את השאילתות למקור שנבחר על ידי המשתמש:

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

כדי לרשום את המתאם כשמפעילים את הווידג'ט, משתמשים בשיטה setAdapter() כשיוצרים את 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();

קוד ה-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.

אבטחת ממשק החיפוש

תוצאות החיפוש מכילות מידע רגיש מאוד. פועלים לפי השיטות המומלצות לאבטחת אפליקציות אינטרנט, במיוחד מפני התקפות clickjacking.

מידע נוסף זמין בפרויקט המדריך של OWASP

הפעלת ניפוי באגים

משתמשים ב-interceptSearchRequest כדי להפעיל את ניפוי הבאגים בווידג'ט החיפוש. לדוגמה:

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

  return request;