다음을 통해 공유


자습서: Bing Image Search API를 사용하여 단일 페이지 앱 만들기

경고

2020년 10월 30일에 Bing Search API가 Azure AI 서비스에서 Bing Search Services로 이동되었습니다. 이 문서는 참조용으로만 제공됩니다. 업데이트된 문서는 Bing search API 문서를 참조하세요. Bing 검색을 위한 새 Azure 리소스 만들기에 대한 지침은 Azure Marketplace를 통해 Bing Search 리소스 만들기를 참조하세요.

Bing Image Search API를 사용하면 웹에서 고품질 관련 이미지를 검색할 수 있습니다. 이 자습서를 사용하여 검색 쿼리를 API에 보낼 수 있는 단일 페이지 웹 애플리케이션을 빌드하고 웹 페이지 내에 결과를 표시합니다. 이 자습서는 Bing Web Search에 대한 해당 자습서와 유사합니다.

자습서 앱은 다음 방법을 보여 줍니다.

  • JavaScript에서 Bing Image Search API 호출 수행
  • 검색 옵션을 사용하여 검색 결과 개선
  • 검색 결과 표시 및 페이징
  • API 구독 키와 Bing 클라이언트 ID를 요청 및 처리합니다.

필수 구성 요소

  • 최신 버전의 Node.js.
  • Node.js의 Express.js 프레임워크. 소스 코드에 대한 설치 지침은 GitHub 샘플 추가 정보 파일에서 사용할 수 있습니다.

사용자 구독 키 관리 및 저장

이 애플리케이션은 웹 브라우저의 영구적 스토리지를 사용하여 API 구독 키를 저장합니다. 키가 저장되지 않은 경우 웹 페이지에서 해당 키를 요청하는 메시지를 사용자에게 표시하고 나중에 사용할 수 있도록 키를 저장합니다. 나중에 키가 API에서 거부되면 앱이 스토리지에서 키를 제거합니다. 이 샘플은 글로벌 엔드포인트를 사용합니다. 리소스의 Azure Portal에 표시되는 사용자 지정 하위 도메인 엔드포인트를 사용할 수도 있습니다.

storeValueretrieveValue 함수를 정의하여 localStorage 개체(브라우저가 지원하는 경우) 또는 쿠키를 사용합니다.

// Cookie names for data being stored
API_KEY_COOKIE   = "bing-search-api-key";
CLIENT_ID_COOKIE = "bing-search-client-id";
// The Bing Image Search API endpoint
BING_ENDPOINT = "https://api.cognitive.microsoft.com/bing/v7.0/images/search";

try { //Try to use localStorage first
    localStorage.getItem;   

    window.retrieveValue = function (name) {
        return localStorage.getItem(name) || "";
    }
    window.storeValue = function(name, value) {
        localStorage.setItem(name, value);
    }
} catch (e) {
    //If the browser doesn't support localStorage, try a cookie
    window.retrieveValue = function (name) {
        var cookies = document.cookie.split(";");
        for (var i = 0; i < cookies.length; i++) {
            var keyvalue = cookies[i].split("=");
            if (keyvalue[0].trim() === name) return keyvalue[1];
        }
        return "";
    }
    window.storeValue = function (name, value) {
        var expiry = new Date();
        expiry.setFullYear(expiry.getFullYear() + 1);
        document.cookie = name + "=" + value.trim() + "; expires=" + expiry.toUTCString();
    }
}

getSubscriptionKey() 함수는 retrieveValue를 사용하여 이전에 저장된 키를 검색하려고 시도합니다. 키가 저장되지 않은 경우 해당 키를 요청하는 메시지를 사용자에게 표시하고 storeValue를 사용하여 키를 저장합니다.


// Get the stored API subscription key, or prompt if it's not found
function getSubscriptionKey() {
    var key = retrieveValue(API_KEY_COOKIE);
    while (key.length !== 32) {
        key = prompt("Enter Bing Search API subscription key:", "").trim();
    }
    // always set the cookie in order to update the expiration date
    storeValue(API_KEY_COOKIE, key);
    return key;
}

HTML <form> 태그 onsubmitbingWebSearch 함수를 호출하여 검색 결과를 반환합니다. bingWebSearchgetSubscriptionKey를 사용하여 각 쿼리를 인증합니다. 이전 정의에 표시된 것처럼 키를 입력하지 않은 경우 getSubscriptionKey는 사용자에게 키를 확인하는 메시지를 표시합니다. 그런 다음, 애플리케이션에서 계속 사용할 수 있도록 키가 저장됩니다.

<form name="bing" onsubmit="this.offset.value = 0; return bingWebSearch(this.query.value,
bingSearchOptions(this), getSubscriptionKey())">

검색 요청 보내기

이 애플리케이션은 onsubmit 특성을 사용하여 newBingImageSearch()를 호출하는 사용자 검색 요청을 처음에 보내는 데 HTML <form>을 사용합니다.

<form name="bing" onsubmit="return newBingImageSearch(this)">

기본적으로 onsubmit 처리기는 false를 반환하여 양식이 제출되지 못하게 합니다.

검색 옵션 선택

[Bing Image Search 양식]

Bing Image Search API는 검색 범위를 좁히고 검색 결과를 필터링하기 위한 여러 필터 쿼리 매개 변수를 제공합니다. 이 애플리케이션의 HTML 양식은 다음 매개 변수 옵션을 사용하고 표시합니다.

옵션 Description
where 검색에 사용되는 지역/국가(위치 및 언어)를 선택하기 위한 드롭다운 메뉴입니다.
query 검색어를 입력할 텍스트 필드입니다.
aspect 대략 정사각형, 너비 또는 높이 등 찾은 이미지의 비율을 선택하는 라디오 단추입니다.
color
when 필요에 따라 가장 최근의 일, 주 또는 월로 검색을 제한하기 위한 드롭다운 메뉴입니다.
safe Bing의 유해 정보 차단 기능을 사용하여 "성인" 결과를 필터링할지 여부를 나타내는 확인란입니다.
count 숨겨진 필드입니다. 각 요청에서 반환할 검색 결과 수입니다. 페이지당 더 적거나 더 많은 결과를 표시하도록 변경합니다.
offset 숨겨진 필드입니다. 요청의 첫 번째 검색 결과에 대한 오프셋으로, 페이징에 사용됩니다. 새 요청 시 0으로 다시 설정됩니다.
nextoffset 숨겨진 필드입니다. 검색 결과를 수신하면 이 필드는 응답에서 nextOffset의 값으로 설정됩니다. 이 필드를 사용하면 후속 페이지에서 겹치는 결과를 방지합니다.
stack 숨겨진 필드입니다. 이전 페이지로 다시 탐색하기 위해 검색 결과 중 이전 페이지의 오프셋에서 JSON 인코딩된 목록

bingSearchOptions() 함수는 앱의 API 요청에서 사용할 수 있는 부분 쿼리 문자열로 이러한 옵션의 형식을 지정합니다.

// Build query options from the HTML form
function bingSearchOptions(form) {

    var options = [];
    options.push("mkt=" + form.where.value);
    options.push("SafeSearch=" + (form.safe.checked ? "strict" : "off"));
    if (form.when.value.length) options.push("freshness=" + form.when.value);
    var aspect = "all";
    for (var i = 0; i < form.aspect.length; i++) {
        if (form.aspect[i].checked) {
            aspect = form.aspect[i].value;
            break;
        }
    }
    options.push("aspect=" + aspect);
    if (form.color.value) options.push("color=" + form.color.value);
    options.push("count=" + form.count.value);
    options.push("offset=" + form.offset.value);
    return options.join("&");
}

요청 수행

검색 쿼리, 옵션 문자열 및 API 키를 통해 BingImageSearch() 함수는 XMLHttpRequest 개체를 사용하여 Bing Image Search 엔드포인트에 대한 요청을 수행합니다.

// perform a search given query, options string, and API key
function bingImageSearch(query, options, key) {

    // scroll to top of window
    window.scrollTo(0, 0);
    if (!query.trim().length) return false;     // empty query, do nothing

    showDiv("noresults", "Working. Please wait.");
    hideDivs("results", "related", "_json", "_http", "paging1", "paging2", "error");

    var request = new XMLHttpRequest();
    var queryurl = BING_ENDPOINT + "?q=" + encodeURIComponent(query) + "&" + options;

    // open the request
    try {
        request.open("GET", queryurl);
    }
    catch (e) {
        renderErrorMessage("Bad request (invalid URL)\n" + queryurl);
        return false;
    }

    // add request headers
    request.setRequestHeader("Ocp-Apim-Subscription-Key", key);
    request.setRequestHeader("Accept", "application/json");
    var clientid = retrieveValue(CLIENT_ID_COOKIE);
    if (clientid) request.setRequestHeader("X-MSEdge-ClientID", clientid);

    // event handler for successful response
    request.addEventListener("load", handleBingResponse);

    // event handler for erorrs
    request.addEventListener("error", function() {
        renderErrorMessage("Error completing request");
    });

    // event handler for aborted request
    request.addEventListener("abort", function() {
        renderErrorMessage("Request aborted");
    });

    // send the request
    request.send();
    return false;
}

HTTP 요청이 성공적으로 완료되면 JavaScript는 “로드” 이벤트 처리기인 handleBingResponse()를 호출하여 성공한 HTTP GET 요청을 처리합니다.

// handle Bing search request results
function handleBingResponse() {
    hideDivs("noresults");

    var json = this.responseText.trim();
    var jsobj = {};

    // try to parse JSON results
    try {
        if (json.length) jsobj = JSON.parse(json);
    } catch(e) {
        renderErrorMessage("Invalid JSON response");
    }

    // show raw JSON and HTTP request
    showDiv("json", preFormat(JSON.stringify(jsobj, null, 2)));
    showDiv("http", preFormat("GET " + this.responseURL + "\n\nStatus: " + this.status + " " +
        this.statusText + "\n" + this.getAllResponseHeaders()));

    // if HTTP response is 200 OK, try to render search results
    if (this.status === 200) {
        var clientid = this.getResponseHeader("X-MSEdge-ClientID");
        if (clientid) retrieveValue(CLIENT_ID_COOKIE, clientid);
        if (json.length) {
            if (jsobj._type === "Images") {
                if (jsobj.nextOffset) document.forms.bing.nextoffset.value = jsobj.nextOffset;
                renderSearchResults(jsobj);
            } else {
                renderErrorMessage("No search results in JSON response");
            }
        } else {
            renderErrorMessage("Empty response (are you sending too many requests too quickly?)");
        }
    }

    // Any other HTTP response is an error
    else {
        // 401 is unauthorized; force re-prompt for API key for next request
        if (this.status === 401) invalidateSubscriptionKey();

        // some error responses don't have a top-level errors object, so gin one up
        var errors = jsobj.errors || [jsobj];
        var errmsg = [];

        // display HTTP status code
        errmsg.push("HTTP Status " + this.status + " " + this.statusText + "\n");

        // add all fields from all error responses
        for (var i = 0; i < errors.length; i++) {
            if (i) errmsg.push("\n");
            for (var k in errors[i]) errmsg.push(k + ": " + errors[i][k]);
        }

        // also display Bing Trace ID if it isn't blocked by CORS
        var traceid = this.getResponseHeader("BingAPIs-TraceId");
        if (traceid) errmsg.push("\nTrace ID " + traceid);

        // and display the error message
        renderErrorMessage(errmsg.join("\n"));
    }
}

중요

성공한 HTTP 요청에는 실패한 검색 정보가 포함될 수 있습니다. 검색 작업 중에 오류가 발생하는 경우 Bing Image Search API는 JSON 응답으로 200 이외의 HTTP 상태 코드 및 오류 정보를 반환합니다. 또한 요청의 속도가 제한된 경우 API에서 빈 응답을 반환합니다.

검색 결과 표시

검색 결과는 renderSearchResults() 함수에 의해 표시되며, 이 함수는 Bing Image Search 서비스에서 반환된 JSON을 사용하고 반환된 이미지 및 관련 검색에서 해당하는 렌더러 함수를 호출합니다.

function renderSearchResults(results) {

    // add Prev / Next links with result count
    var pagingLinks = renderPagingLinks(results);
    showDiv("paging1", pagingLinks);
    showDiv("paging2", pagingLinks);

    showDiv("results", renderImageResults(results.value));
    if (results.relatedSearches)
        showDiv("sidebar", renderRelatedItems(results.relatedSearches));
}

이미지 검색 결과는 JSON 응답 내의 최상위 value 개체에 포함됩니다. 이러한 검색 결과는 결과를 반복하고 각 항목을 HTML로 변환하는 renderImageResults()에 전달됩니다.

function renderImageResults(items) {
    var len = items.length;
    var html = [];
    if (!len) {
        showDiv("noresults", "No results.");
        hideDivs("paging1", "paging2");
        return "";
    }
    for (var i = 0; i < len; i++) {
        html.push(searchItemRenderers.images(items[i], i, len));
    }
    return html.join("\n\n");
}

Bing Image Search API는 각각 최상위 개체에서 사용자 검색 환경을 안내하는 데 도움이 되는 네 가지 유형의 검색 제안을 반환할 수 있습니다.

제안 Description
pivotSuggestions 원래 검색에 포함된 중심 단어를 다른 단어로 바꾸는 쿼리입니다. 예를 들어 “red flowers”를 검색하는 경우 중심 단어는 “red”일 수 있고, 중심 제안은 “yellow flowers”일 수 있습니다.
queryExpansions 용어를 더 추가하여 원래 검색의 범위를 좁히는 쿼리입니다. 예를 들어 “Microsoft Surface”를 검색하는 경우 쿼리가 “Microsoft Surface Pro”로 확장될 수 있습니다.
relatedSearches 또한 원래 검색을 입력했던 다른 사용자가 입력한 쿼리입니다. 예를 들어 “Mount Rainier”를 검색하는 경우 관련 검색은 “Mt. Saint Helens”일 수 있습니다.
similarTerms 원래 검색과 의미가 유사한 쿼리입니다. 예를 들어, "kittens"를 검색하는 경우 비슷한 용어로 "cute"가 있을 수 있습니다.

이 애플리케이션은 relatedItems 제안만 렌더링하고 결과 링크를 페이지의 사이드바에 배치합니다.

검색 결과 렌더링

이 애플리케이션에서 searchItemRenderers 개체에는 각 종류의 검색 결과에 대한 HTML을 생성하는 렌더러 함수가 포함됩니다.

searchItemRenderers = {
    images: function(item, index, count) { ... },
    relatedSearches: function(item) { ... }
}

이러한 렌더러 함수는 다음 매개 변수를 허용합니다.

매개 변수 Description
item URL 및 해당 설명과 같은 항목의 속성을 포함하는 JavaScript 개체입니다.
index 해당 컬렉션 내에서 결과 항목의 인덱스입니다.
count 검색 결과 항목 컬렉션에 있는 항목 수입니다.

indexcount 매개 변수는 결과에 번호를 매기고, 컬렉션에 대한 HTML을 생성하고, 콘텐츠를 구성하는 데 사용됩니다. 특히 다음을 수행합니다.

  • 이미지 썸네일 크기를 계산합니다(높이가 90픽셀에 고정된 반면 너비는 최소 120픽셀로 다름).
  • HTML <img> 태그를 빌드하여 이미지 썸네일을 표시합니다.
  • 이미지와 해당 이미지를 포함하는 페이지에 연결되는 HTML <a> 태그를 빌드합니다.
  • 이미지와 해당 이미지가 있는 사이트에 대한 정보를 표시하는 설명을 빌드합니다.
    images: function (item, index, count) {
        var height = 120;
        var width = Math.max(Math.round(height * item.thumbnail.width / item.thumbnail.height), 120);
        var html = [];
        if (index === 0) html.push("<p class='images'>");
        var title = escape(item.name) + "\n" + getHost(item.hostPageDisplayUrl);
        html.push("<p class='images' style='max-width: " + width + "px'>");
        html.push("<img src='"+ item.thumbnailUrl + "&h=" + height + "&w=" + width +
            "' height=" + height + " width=" + width + "'>");
        html.push("<br>");
        html.push("<nobr><a href='" + item.contentUrl + "'>Image</a> - ");
        html.push("<a href='" + item.hostPageUrl + "'>Page</a></nobr><br>");
        html.push(title.replace("\n", " (").replace(/([a-z0-9])\.([a-z0-9])/g, "$1.<wbr>$2") + ")</p>");
        return html.join("");
    }, // relatedSearches renderer omitted

썸네일 이미지의 heightwidth<img> 태그와 썸네일의 URL에 있는 hw 필드 둘 다에 사용됩니다. 따라서 Bing은 정확히 해당 크기의 썸네일을 반환할 수 있습니다.

클라이언트 ID 유지

Bing Search API의 응답에는 후속 요청과 함께 API로 다시 전송되어야 하는 X-MSEdge-ClientID 헤더가 포함될 수 있습니다. 여러 개의 Bing Search API를 사용하는 경우 모두 동일한 클라이언트 ID를 사용해야 합니다(가능한 경우).

X-MSEdge-ClientID 헤더를 제공하면 Bing API가 모든 사용자 검색을 연결할 수 있어, 다음과 같은 경우 유용합니다.

첫째, Bing 검색 엔진이 이전 컨텍스트를 검색에 적용하여 사용자에게 보다 만족스러운 결과를 찾을 수 있습니다. 예를 들어 사용자가 이전에 항해와 관련된 용어를 검색한 경우 나중에 “knots”를 검색하면 항해에 사용되는 노트에 대한 정보가 우선적으로 반환될 수 있습니다.

둘째, Bing을 통해 임의로 선택된 사용자가 새로운 기능이 폭넓게 사용되기 전에 이러한 새 기능을 경험해볼 수 있습니다. 각 요청에 동일한 클라이언트 ID를 제공하면 기능을 볼 수 있도록 선택된 사용자에게 항상 기능이 표시됩니다. 클라이언트 ID가 없으면 사용자의 검색 결과에서 기능이 임의로 나타나거나 사라지는 것처럼 보일 수 있습니다.

브라우저 보안 정책(CORS) 때문에 JavaScript에서 X-MSEdge-ClientID 헤더를 사용하지 못할 수 있습니다. 이러한 제한은 검색 응답의 원본이 해당 응답을 요청한 페이지와 다른 경우에 발생합니다. 프로덕션 환경에서는 웹 페이지와 동일한 도메인에 대해 API 호출을 수행하는 서버 쪽 스크립트를 호스트하여 이 정책 문제를 해결해야 합니다. 스크립트의 원본은 웹 페이지와 동일하므로 JavaScript에서 X-MSEdge-ClientID 헤더를 사용할 수 있습니다.

참고

그래도 프로덕션 웹 애플리케이션의 경우 서버 쪽에서 요청을 수행해야 합니다. 그렇지 않은 경우 Bing Search API 키를 웹 페이지에 포함해야만 원본을 보는 누구나 사용할 수 있게 됩니다. 권한 없는 사람이 수행한 요청을 포함하여 API 구독 키를 통한 모든 사용량에 요금이 청구되므로, 키를 노출하지 않는 것이 중요합니다.

개발 목적으로 CORS 프록시를 통해 Bing Web Search API 요청을 수행할 수 있습니다. 이러한 프록시의 응답에는 응답 헤더를 허용하고 JavaScript에서 응답 헤더를 사용할 수 있게 해주는 Access-Control-Expose-Headers 헤더가 포함됩니다.

자습서 앱이 클라이언트 ID 헤더에 액세스할 수 있도록 CORS 프록시를 쉽게 설치할 수 있습니다. 먼저 Node.js가 없는 경우 설치합니다. 그런 다음, 명령 창에서 다음 명령을 실행합니다.

npm install -g cors-proxy-server

다음으로, HTML 파일에서 Bing Web Search 엔드포인트를 변경합니다.
http://localhost:9090/https://api.cognitive.microsoft.com/bing/v7.0/search

마지막으로 다음 명령을 사용하여 CORS 프록시를 시작합니다.

cors-proxy-server

자습서 앱을 사용하는 동안에는 명령 창을 열어 두세요. 창을 닫으면 프록시가 중지됩니다. 검색 결과 아래의 확장 가능한 HTTP 헤더 섹션에서 여러 X-MSEdge-ClientID 헤더를 볼 수 있으며 요청마다 동일한지 확인합니다.

다음 단계

참고 항목