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

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

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

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

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

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

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

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

במאמר יצירת חוויית חיפוש בהתאמה אישית מוסבר איך יוצרים אפליקציית חיפוש.

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

בנוסף לשלבים שמפורטים בקטע הגדרת גישה ל-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>

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

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

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

מידע נוסף זמין במאמר שימוש בכניסה באמצעות חשבון 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');
}

כדי לרשום את המתאם בזמן האיפוס של הווידג'ט, משתמשים ב-method‏ 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();

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

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

כדי ליצור רכיב מותאם אישית להצעה, לקונטיינר של מאפיינים או לתוצאת חיפוש, יוצרים ומרשמים מתאם שמטמיע את 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:

  • צריך לצרף את הכיתה cloudsearch_facet_bucket_clickable ב-CSS לרכיב שבו המשתמשים לוחצים כדי להפעיל או להשבית קטגוריה.
  • צריך לעטוף כל קטגוריה ברכיב מכיל עם הכיתה cloudsearch_facet_bucket_container ב-CSS.
  • אי אפשר להציג את הקטגוריות בסדר שונה מהסדר שבו הן מופיעות בתגובה.

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

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

מטמיעים מתאם עם השיטה 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;
}

כדי לרשום את המתאם בזמן האינטוליזציה של הווידג'ט, משתמשים ב-method‏ setAdapter() בזמן ה-build של 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;