본문 바로가기

TechLog

구글 데스크탑을 이용한 인트라넷 파일 검색 웹 서비스 구현

※ 추가(2012.08) 
오랫동안 안 들여다보고 있었더니... Google Desktop이 depreciated 되었네요 (...) 혹시 설치 파일을 구하려고 하시는 분이 있을지 모르겠다는 생각에 최종 버전(5.9.1005.12335)을 첨부해 둡니다. 혹은 다음 URL에서 다운받을 수 있습니다: http://www.filehippo.com/download_google_desktop/


GoogleDesktopSetup5.9.1005.12335.exe



※ 추가(2010.05) 
"file://" url scheme을 사용한 링크의 경우 IE에서만 동작하는 것을 확인하였습니다. (예전에는 firefox에서 동작하는 것을 언뜻 확인했었는데, 보안 문제 때문에 http -> file scheme으로의 접근을 막은 것 같네요)

결론만 말씀드리자면 ... IE를 제외한 기타 브라우저에서도 동작하게 하려면 일반적인 파일 공유 링크(즉, file:// scheme) 대신에 http 주소로 매핑을 해 줘야 합니다. 이렇게 구현하려면 실제 파일에 대한 요청을 url 형태로 요청할 수 있게 해주는(예를 들면 file://abc/c:/test.txt -> http://abc/requestfile.aspx?file=/c:/test.txt 이런 식으로 변환해주면 되겠죠) 일종의 변환 페이지를 구현해줘야겠죠.

혼란을 드려 죄송합니다. 저 부분은 나중에 시간 좀 널럴해지면 구현해볼께요.


구현한 것은 언제인지 기억도 안 나고, 작년 이맘때쯤 마이크로소프트웨어에 기고했던 내용. 묵혀두면 뭐하나 싶어 뒤늦게 공개해본다. (지금까지 비밀글로 되어 있었는데 비밀글로 해놨었는지도 모르고 있었…)

개요만 간단히 설명하자면 ‘구글 데스크탑을 파일 서버에 설치해서 인덱싱을 하게 한 후, 웹(=인트라넷)을 통해 구글 데스크탑의 검색 기능을 제공하는 서비스의 구현’. 사실 그리 복잡한 내용이 아니라서 http 호출과 xml 파싱에 대한 간단한 지식만 있다면 누구든 충분히 할 수 있는 일이지만, ‘머리를 굴려보면 이런 것도 나온다’정도의 가벼운 감각으로 읽어주시면 감사할 뿐.

참고로, 이 글의 내용은 Windows 2003 Server, Visual Studio 2005, 구글 데스크탑이 설치된 환경에서 작성되었으며, C#/ASP.NET을 이용해 개발했음을 미리 알려둔다.

 

- 발상

한줄요약 : 필자는 서버의 파일을 검색하는, 구글 데스크탑 비슷한 ASP.NET 기반의 인트라넷 사이트를 만들고 싶었다.



- 고민

한줄요약에서 보다시피, 필자가 처음 이 서비스를 구현하려고 했을 때 모델로 삼은 것은 구글 데스크탑이었다. 다만 구글 데스크탑이 '내 PC의 파일을 검색, 접근한다'는 개념이라면, 이 사이트는 '서버의 파일을 검색, 접근한다'라는 개념을 갖고 있는 것 뿐이었다. 당연하게도, 처음부터 '데스크탑 검색 프로그램의 결과물을 웹 페이지에 출력할 수 없을까?'하는 생각을 떠올렸다.

몇 가지 데스크탑 검색 프로그램을 검토해 보았다. 구글 데스크탑 검색, 네이버 내PC검색, 엠파스 데스크톱 검색, 마이크로소프트의 Windows Desktop Search 등을 검토했다. 하지만 애초에 모델로 삼았던 프로그램이 구글 데스크톱이기도 했고 필자가 주력으로 사용하는 데스크탑 검색 프로그램이 구글 데스크탑이었으므로, 이것저것 더 고려하는 대신 결국 구글 데스크탑 검색을 사용하는 것으로 결정했다.

최종적으로는 이런 형태의 화면을 구상했다(지금 이 이미지는 결과물이지만, 애초 구상과 별다른 점도 없다) :

"사용자



BlogEngine.NET이라는 이름의 공개 블로그 프로젝트를 사용해서 블로그를 만들고, 블로그 메뉴에 '파일 검색'이라는 항목을 추가하여 파일 서버의 파일을 검색하는 기능이 있는 웹 페이지를 구상하였다. (참고로 블로그 엔진에 관심이 있다면 http://www.dotnetblogengine.net 를 방문해보자. 이 글에서는 블로그 서비스 자체에 대해서는 다루지 않을 것이므로, 이 프로젝트에 대한 설명은 생략하도록 하겠다)
 
그런 다음 이 페이지에 필요한 핵심 기능을 몇 가지로 압축했다 :

1. 서버의 특정 경로(미리 구성 가능한)에 존재하는, 다양한 형식(doc, xls, ppt, pdf, hwp, html ...)의 파일을 검색
2. 검색된 파일을 다운로드, 혹은 해당 파일이 있는 경로(폴더) 열기
3. 검색된 파일의 일부 내용을 출력


위 기능들을 살펴보면 알겠지만, 일반적인 데스크탑 검색 도구라면 다들 제공하고 있는 기능이다. 남은 문제는 단지 데스크탑 검색 도구가 외부 API를 통해서 이러한 기능을 지원하고 있는가 하는 것 뿐이다. 구글 데스크탑의 검색(Query) API에 관련된 내용은 http://code.google.com/apis/desktop/docs/queryapi.html 페이지에서 살펴볼 수 있는데, 이 중 필자가 사용한 방법은 'HTTP/XML-Based Query API' 항목에 설명되어 있는 방법이다. (좀 더 관심이 있는 독자는 http://desktop.google.com/downloadsdksubmit 에서 구글 데스크탑 SDK를 다운로드 받아서 내용을 살펴보도록 하자)

개발자의 성향에 따라서 컴퍼넌트 방식의 API가 더 편리할 수도 있겠으나, 필자의 경우에는 XML로 검색 결과를 얻은 후에, XML 문서를 가공하는 편이 좀 더 간편하다고 생각해서 이 방법을 택했다. (실은 여러 시행착오 끝에 얻은 결론이지만, 시행착오의 과정은 별로 재미도 없고 얻을만한 내용도 없으므로 넘어가도록 하자)


HTTP/XML-Based Query API 방식을 이해하기 위해서, 먼저 구글 데스크탑에서 검색을 수행할 때 무슨 일이 일어나는지 살펴보도록 하자.



- 시도

1. 구글 데스크탑의 검색 결과 살펴보기

간단히 구글 데스크탑에서 'test'라는 문자열을 검색해 보자. 다음과 같은 화면을 볼 수 있을 것이다.

"사용자


브라우저의 주소창에는 다음과 같은 URL이 입력되어 있는 것을 볼 수 있다 :

http://127.0.0.1:4664/search?q=test&flags=68&num=10&s=PGMUglcWBH6D5jCdS8KnpmaAhTQ


이 URL을 몇 개의 부분으로 나눠서 살펴보자.

http://127.0.0.1:4664/search
127.0.0.1은 로컬 컴퓨터(구글 데스크탑이 실행되는 컴퓨터 자신)를 가리키는 IP 주소이며, 구글 데스크탑은 4664 포트를 통해서 구글 데스크탑 서비스를 제공한다. 결과적으로 이 URL이 구글 데스크탑의 검색 URL이며, 로컬 컴퓨터 외의 다른 컴퓨터에서는 접근이 불가능하다. (사실 다른 컴퓨터에서 접근이 가능하다면 타인이 내 컴퓨터의 자료를 마음껏 접근할 수 있다는 보안 문제는 둘째치고, 이 글 자체도 의미가 없을 것이다)

q=test
설명할 필요도 없겠지만, 검색어이다. 한글 등은 escape 처리를 해 주어야 한다.

flags=68
각종 옵션(파일, 웹 링크 검색, 정렬 조건 등)을 나타낸다.

num=10
얻어올 검색 결과의 갯수를 나타낸다. 위 URL에는 나타나 있지 않지만, start라는 인수를 사용해서 몇 번째 검색 결과부터 검색 결과를 얻을지 지정할 수 있다.

s=PGMUglcWBH6D5jCdS8KnpmaAhTQ
일종의 보안 코드로, 현재 검색 요청이 유효한 것인지 검증하는 역할도 한다.

 

그런 다음, 위의 url의 끝에 &format=xml이란 문자열을 덧붙여서 조회해 보자. (필자의 경우에는 URL이 http://127.0.0.1:4664/search?q=test&flags=68&num=10&s=PGMUglcWBH6D5jCdS8KnpmaAhTQ&format=xml 이었다) 그러면 다음과 같은 화면을 볼 수 있다 :

"사용자

일단 XML 문서의 내용이 의미하는 바는 나중에 살펴보도록 하고, 위의 URL만 살펴보면 해야할 일이 무척 간단해 보인다. 단순히 검색어와 몇 번째의 검색 결과를 가져올지, 몇 개의 검색 결과를 가져올지를 URL의 인자에 지정해주면 되지 않겠는가?

일이 그렇게 간단하지가 않다. 아까의 검색 결과에서 2페이지를 클릭해보면, 다음과 같이 URL이 변경되는 것을 볼 수 있다 :

http://127.0.0.1:4664/search?q=test&flags=68&num=10&start=10&s=2RV_FSZnsij-a8luW3hXgvZep2c

위 URL에서는 num, start 인자가 변경된 것을 볼 수 있다. (10번째 검색 결과로부터 10개의 검색 결과를 가져온다는 의미이다) 하지만, s 인자의 값이 1페이지의 검색결과와는 다른 값을 갖고 있는 것을 볼 수 있다. 여기서 URL의 num이나 start 인자를 직접 수정하면, 구글 데스크탑은 URL을 검증한 후 잘못된 접근이라고 판단한다.



"사용자

직접 URL을 수정해서 검색을 시도하면 위와 같은 오류 메시지를 볼 수 있다.


이는 구글 데스크탑이 로컬 컴퓨터에서만 검색을 수행하도록 포트 포워딩 등을 통해 외부 컴퓨터가 구글 데스크탑에 접근하려는 시도를 막기 위한 조치로 보인다. 이러한 보안 제약사항 때문에, 구글 데스크탑 검색 API는 검색을 위한 URL을 별도로 제공하고 있다. 이 검색 URL은 다음 레지스트리에서 찾아볼 수 있다 :

HKEY_CURRENT_USER\Software\Google\Google Desktop\API\search_url

위 레지스트리의 값을 이용해 검색 URL을 만들면 된다. 레지스트리의 값은 다음과 같은 형태이다 (s 인자의 값은 가끔 변경되기 때문에 검색을 수행할 때마다 레지스트리의 값을 읽어들여서 처리해야 한다) :

http://127.0.0.1:4664/search&s=1ftR7c_hVZKYvuYS-RWnFHk91Z0?q=

그러면 이제 다음과 같은 URL을 통해 검색을 수행할 수 있으며, 검색 결과를 XML 문서 형태로 얻을 수 있게 된다 :

http://127.0.0.1:4664/search&s=1ftR7c_hVZKYvuYS-RWnFHk91Z0?q=test&num=10&start=0&format=xml



2. ASP.NET에서 구글 데스크탑 URL 호출하고 XML 문서 처리하기

이 시점에서, ASP.NET 페이지에서 처리해야 할 일을 한 번 요약해보자 :

- 구글 데스크탑이 파일을 검색할 경로를 설정한다.
- 검색어를 입력받는다.
- 구글 데스크탑의 URL을 레지스트리에서 읽어온다.
- URL을 호출, 검색 결과를 얻는다.
- 검색 결과를 표시한다.

쓸만한 파일 검색 페이지를 만들기 위해 해야할 일은 위 목록이 전부가 아니지만, 이 글에서는 단순히 구글 데스크탑의 API를 통해 검색 결과를 가져온 후 그 결과를 처리하는 방법을 집중적으로 살펴볼 것이다. 한 항목씩 내용을 검토해보자.


- 구글 데스크탑이 파일을 검색할 경로를 설정한다.

"사용자

해당 컴퓨터에서 실행중인 구글 데스크탑 아이콘을 우클릭하고 환경설정을 클릭하면, 환경설정 페이지가 나타난다. 로컬 색인생성 항목을 보면, 다음과 같이 검색 위치와 검색 제외 항목이 있는 것을 볼 수 있는데, 여기에 검색에 포함될 경로 및 검색에서 제외할 경로를 지정하면 된다. 다만 여기서 생각해볼 문제가, C:\ 같은 로컬 드라이브를 검색할 수 있게 할지의 여부이다.

네트워크 폴더 경로(\\123.123.123.123\folder 형태의)를 사용할 경우에는 검색 결과의 url에 해당 경로가 그대로 출력되기 때문에, 클라이언트가 해당 네트워크 폴더에 접근할 수 있는 상태라면 얼마든지 접근할 수 있지만, 로컬 드라이브의 경우에는 C:\test.txt(브라우저에는 file:///c/test.txt와 같은 형태로 표시되긴 하지만)라는 파일 경로가 그대로 전달된다. 이 경로를 적당히 고쳐서 네트워크 폴더 경로 형태로 만들던가, 파일을 http를 통해 다운로드할 수 있도록 별도의 페이지를 구현할 수도 있을 것이다. 하지만 이 글에서는 검색 기능 구현의 단순화를 위해, 검색 대상 폴더를 네트워크 폴더 경로로 지정했다고 가정하겠다. 또한 네트워크 폴더를 사용할 경우, 이 검색 페이지가 구현된 웹 서버 뿐만 아니라 타 서버에도 접근할 수 있을 것이므로 검색 범위를 확장하기에 용이할 것이다.

이와 같은 검색 폴더 설정은 구글 데스크탑의 환경 설정 메뉴에서 지정할 수 있다.

* 각 메뉴의 명칭은 구글 데스크탑의 버전에 따라 조금씩 다르다.

"사용자



- 검색어를 입력받는다.

Visual Studio 2005에서 ASP.NET 웹 사이트를 생성하고, search.aspx라는 이름의 웹페이지를 만들어보자 :

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Search.aspx.cs" Inherits="Search" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>검색 페이지</title>
</head>
<body>
    <form id="form1" action="Search.aspx" method=get>
    <div>
  <input type=text name="q" value="<%=Request["q"]%>"/>
  <input type=submit value="검색" /> 
    </div>
    <div id="divResult" runat="server">
    </div>
    </form>
</body>
</html>

Search.aspx


일단 화면에는 검색에 필요한 텍스트 박스와 검색 버튼을 둔다. q라는 이름의 텍스트 박스의 값이 <%=Request["q"]%>로 되어 있는 것을 볼 수 있는데, 이는 URL로 전달되는 q 인자(=검색어)를 계속 텍스트 박스에 표시하기 위해 넣어둔 것이다. 그리고 검색 결과를 출력할 Div 태그(divResult)를 배치한다. 그런 다음 search.aspx.cs 파일을 편집해서 필요한 코드를 추가한다.

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml;
using System.Collections;
using System.Text;

public partial class Search : System.Web.UI.Page
{
    const string strHtmlTemplate =
        @"<p>- 검색 건수 : {0} 건</p>" +
        @"<p>{1}</p>" +
        @"<p>{2}</p>";

    protected void Page_Load(object sender, EventArgs e)
    {
        int i = 0;
        string num = Request["num"];
        string start = Request["start"];
        string q = Request["q"];

        if (null == num) num = "10";
        if (null == start) start = "0";
        if (null == q) q = "";

        // 검색어가 있으면 검색 시작
        if ("" == q.Trim()) return;
        XmlDocument xDoc = GetResultXML(q, int.Parse(start), int.Parse(num));

        // 검색 결과 분석
        XmlNode root = xDoc.DocumentElement;
        int total_count = int.Parse(root.Attributes["count"].Value);
        IEnumerator iEnum = root.GetEnumerator();
      
        // 검색 결과 html을 저장할 문자열 변수
        StringBuilder sb_result = new StringBuilder();
       
        // 검색 결과를 한 항목씩 가져와서 html로 변환한다.
        XmlNode result;
        while (iEnum.MoveNext())
        {
            result = (XmlNode)iEnum.Current;
            string url = "";
            string snippet = "";
            for (i=0; i < result.ChildNodes.Count; i++)
            {
                if ("url" == result.ChildNodes[i].Name)
                    url = result.ChildNodes[i].InnerText;
                if ("snippet" == result.ChildNodes[i].Name)
                    snippet = result.ChildNodes[i].InnerText;
            }
            sb_result.Append(GetDisplayItemHTML(url, snippet));
        }

        // 페이징 정보를 저장할 문자열 변수
        StringBuilder sb_search = new StringBuilder();

        // 검색 결과 페이징
        int startitem = int.Parse(start);
        int numitem = int.Parse(num);
        int curPage = (startitem / numitem) + 1;
        int endPage = (total_count - 1) / numitem + 1;

        // 검색어를 인코딩한다.
        q = Server.UrlEncode(q);

        // 이전 10페이지 링크
        if ( curPage > 10 )
            sb_search.Append(
            string.Format("<a href='?q={0}&num={1}&start={2}'>&lt;&lt;</a> &nbsp;",
            q, numitem, ((((startitem) / (numitem * 10)) * numitem * 10 - numitem))));

        for (i = ((curPage - 1) / 10) * 10 + 1; i < ((curPage - 1) / 10) * 10 + 11; i++)
        {
            // 마지막 페이지 지났으면 빠져나감
            if (endPage < i)
                break;

            // 현재 보여주고 있는 페이지는 링크 표시 안함
            if(curPage==i)
                sb_search.Append(string.Format("{0} &nbsp;", i));
            else
                sb_search.Append(
                    string.Format("<a href='?q={0}&num={1}&start={2}'>{3}</a> &nbsp;",
                    q, numitem, (i - 1) * numitem, i));
        }

        // 다음 10페이지 링크
        if (endPage >= i)
            sb_search.Append(
            string.Format("<a href='?q={0}&num={1}&start={2}'>&gt;&gt;</a> &nbsp;",
            q, numitem, ((((startitem) / (numitem * 10)) * numitem * 10 + numitem * 10 ))));

        // 화면에 결과 표시
        divResult.InnerHtml = string.Format(strHtmlTemplate,
                total_count.ToString(), sb_result.ToString(), sb_search.ToString()
                );
       
    }

    // url, description을 가지고 한 항목의 결과 html을 생성한다
    string GetDisplayItemHTML(string url, string description)
    {
        string result = "";
       
        // 파일에 접근 가능한 경로 구함
        string filepath = new Uri(url).LocalPath.Replace("/", "\\");
       
        // 검색된 파일이 압축파일 내에 포함되어 있는지 여부
        bool isZipped = filepath.ToLower().Contains(".zip\\");

        // 화면에 표시할 파일 이름 얻기(너무 길면 자른다)
        int filelength_to_display = 70;
        string fileurl_to_display = null ;
        if (filepath.Length > filelength_to_display)
            fileurl_to_display =
                "..." + filepath.Substring(filepath.Length - filelength_to_display, filelength_to_display);
        else
            fileurl_to_display = filepath;

        // 검색된 파일이 압축되어 있을 경우 해당 압축파일을 링크해야 한다.
        if (isZipped)
            filepath = filepath.Substring(0, filepath.ToLower().IndexOf(".zip") + 4);

        // 해당 파일이 있는 폴더의 주소를 알아낸다.
        string folderpath = filepath.Substring(0, filepath.LastIndexOf("\\"));

        result = string.Format(
            @"<p><a href='{0}'>{1}</a> <a href='{2}'>폴더 열기</a><br />{3}",
            filepath, fileurl_to_display, folderpath, description
            );
        return result;
    }

    // 검색 주소를 가져온다.
    string GetSearchAddress()
    {
        // Adminstrator 계정의 HKEY_CURRENT_USER 레지스트리 키 이름 변수
        string HKEY_USERS_ADMINISTRATOR = null;

        // HKEY_USERS의 항목들을 가져온다
        string[] arrHKEYUSERS = Microsoft.Win32.Registry.Users.GetSubKeyNames();
        foreach (string HKEY_USER in arrHKEYUSERS)
        {
            // 끝이 -500으로 끝나는 키가 Administrator의 HKEY_CURRENT_USER 레지스트리 키에 해당한다.
            if (HKEY_USER.EndsWith("-500"))
                HKEY_USERS_ADMINISTRATOR = Microsoft.Win32.Registry.Users.Name + @"\" + HKEY_USER;
        }
        // 레지스트리에서 검색 URL 주소를 얻는다.
        return
            (string)Microsoft.Win32.Registry.GetValue(
            HKEY_USERS_ADMINISTRATOR + @"\Software\Google\Google Desktop\API", "search_url", "");

    }

    // 검색 결과를 가져오는 함수
    XmlDocument GetResultXML(string q, int first_item_to_request, int count_to_request)
    {
        // 구글 데스크탑 검색 URL을 얻는다.
        string Base_Address = GetSearchAddress();

        // 실제 검색 결과를 반환하는 URL 주소를 구성한다.
        string Search_Address =
            string.Format("{0}{1}&start={2}&num={3}&format=xml",
            Base_Address, System.Uri.EscapeUriString(q), first_item_to_request, count_to_request
        );

        // 검색 결과를 얻어 XML 문서 객체를 생성한다.
        XmlDocument xDoc = new XmlDocument();
        xDoc.Load(Search_Address);
        return xDoc;
    }
}

Search.aspx.cs



- 구글 데스크탑의 검색 URL을 레지스트리에서 읽어온다.

검색 URL을 레지스트리에서 읽어들이는 GetSearchAddress() 메서드를 살펴보자. 간단히 이 함수의 코드를 설명하자면, 컴퓨터에 administrator로 로그온한 상태에서의 HKCU\Software\Google\Google Desktop\API\search_url 레지스트리 키 값을 가져오는 것이다. 하지만 처리하는 과정을 보면 HKEY_USERS 레지스트리 키에서 '-500'으로 끝나는 키 이름을 가져온 후에, 해당 키 아래의 Software\Google\Google Desktop\API\search_url 레지스트리 키를 가져오는 식으로 약간 번잡하게 되어 있는 것을 볼 수 있다.

위와 같이 처리하는 데에는 몇 가지의 이유가 있는데, 기본적으로 ASP.NET의 프로세스(w3wp.exe)는 위 코드를 수행할 때 네트워크 서비스 계정의 권한으로 코드를 수행하며, 구글 데스크탑은 현재 시스템에 로그온되어 있는 사용자에게만 search_url 레지스트리를 제공(로그오프하면 레지스트리가 사라진다)하므로, 실제로 시스템에 로그온할 수 있는(꼭 Administrator가 아니더라도) 로컬 계정의 레지스트리를 읽어야 하기 때문이다.

HKEY_USERS 레지스트리 키 아래에 저장되는 각 계정의 레지스트리(참고로, 이들 각각의 레지스트리는 각 계정의 HKEY_CURRENT_USER 레지스트리와 동일하다) 이름을 실제로 살펴보면 'S-1-5-21-1716890576-4157496229-2455825860-500'와 같은 이름으로 되어 있다. 이 중에서 마지막 문자열이 '500'으로 끝나는 키 이름이 Administrator 계정을 나타내므로, 이 레지스트리 키의 값을 읽어들이는 것이다.

하지만 위와 같은 코드를 작성하고, 곧바로 레지스트리를 읽어들이려고 시도하면 다음과 같은 에러가 발생한다 :

"사용자

이는 아까 설명했던, ASP.NET 프로세스를 수행하고 있는 '네트워크 서비스' 계정이 해당 레지스트리 키에 대한 권한을 갖고 있지 않기 때문에 발생하는 에러다. 이 오류를 해결하려면 다음과 같이 레지스트리 키에 대한 접근 권한을 설정해주어야 한다 :

"사용자



"사용자

위 화면에서는 시스템의 모든 계정을 나타내는 Everyone에 읽기 권한을 주고 있지만, 실제로는 네트워크 서비스 계정(NETWORK SERVICE라는 이름으로 나타난다)에 읽기 권한을 주어도 된다.

함수 구현 및 설정이 올바로 완료되면, 위에서 설명했던 검색 URL('http://127.0.0.1:4664/search&s=1ftR7c_hVZKYvuYS-RWnFHk91Z0?q=' 형태의)을 얻을 수 있다. 이 URL에 검색어와 가져올 검색어 목록의 위치(몇 번째 검색 결과, 몇 개의 검색 결과)를 지정한 후 HTTP 요청을 전송하면 된다.


- URL을 호출, 검색 결과를 얻는다.

GetResultXML() 함수를 살펴보자. 이 함수는 검색 URL 주소를 읽어들여서 검색 결과에 필요한 URL을 생성하고, 검색 결과 XML 문서를 얻어오는 역할을 한다.

GetSearchAddress() 함수를 통해 검색 URL을 얻고, 검색어와 검색 결과 건수를 조합해서 URL을 호출, 최종적으로 XML 문서 객체를 생성한다. System.Xml.XmlDocument 객체는 Load 함수를 통해서 특정 URL의 XML 문서를 직접 읽어들일 수 있는 기능을 제공하기 때문에 다루기가 편리하다.



- 검색 결과를 표시한다.

클래스의 맨 앞에 선언되어 있는 strHtmlTemplate 내부 상수는 검색 결과를 나타낼 형식을 보여준다. 각각의 대입자(Placeholder, {0}, {1} 등)는 각각 전체 검색 결과 건 수, 검색 결과 목록, 페이지 목록(게시판에서의 페이지를 생각하면 될 것이다)에 대입될 것이다.

이제는 Form_Load() 함수를 살펴보자. 먼저 URL로 전달된 num, start, q 인자를 Request 객체를 통해 받아들여서 변수에 할당하며, 페이지와 관련된 인자(num, start)에 값이 없을 경우 기본값을 할당하게 된다.

그런 다음 검색어가 공백으로만 입력되었는지 확인하고, 검색어가 있으면 위에서 설명한 GetResultXML() 함수를 사용해서 검색 결과를 XML 문서로 가져온다. 여기서 XML 검색 결과의 구조를 잠시 살펴보면 다음과 같다 :

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?> 
<!-- 
Content-type: fix-mhtml
-->
 <results count="365">
 <result>
  <category>file</category>
  <doc_id>33711</doc_id>
  <event_id>33711</event_id>
  <title>Settings.Designer.cs</title>
  <url>\\192.168.0.203\d$\My Documents\__강의및스터디\sql, as\MSLabs\testSQLTryCatch\testSQLTryCatch\Properties\Settings.Designer.cs</url>
  <flags>2</flags>
  <time>128557187955460000</time>
  <snippet>global:System.Configuration.ApplicationSettingsBase.Synchronized(new Settings( public static Settings Default {get {return defaultInstance; 192 168 0 203 <b>test</b> SQLTry Catch <b>test</b> SQLTry Catch</snippet>
  <icon>/file.gif</icon>
  <cache_url>http://127.0.0.1:4664/redir?url=http%3A%2F%2F127%2E0%2E0%2E1%3A4664%2Fcache%3Fevent%5Fid%3D33711%26schema%5Fid%3D4%26q%3Dtest%26s%3DlAOkv4OgfO%5F7RlcyhwMbgWC1JnE&src=1&schema=4&s=pj0aMi_9LJI0mlJ_RwF5tkwuZ7U</cache_url>
  </result>
 <result>
  <category>file</category>
  <doc_id>33707</doc_id>
  <event_id>33707</event_id>
  <title>testSQLTryCatch.Properties.Resources.resources</title>
  <url>\\192.168.0.203\d$\My Documents\__강의및스터디\sql, as\MSLabs\testSQLTryCatch\testSQLTryCatch\obj\Debug\testSQLTryCatch.Properties.Resources.resources</url>
  <flags>0</flags>
  <time>128557187958120000</time>
  <icon>/file.gif</icon>
  <cache_url>http://127.0.0.1:4664/redir?url=http%3A%2F%2F127%2E0%2E0%2E1%3A4664%2Fcache%3Fevent%5Fid%3D33707%26schema%5Fid%3D8%26q%3Dtest%26s%3DKLUpkwEMNOeElpZGqb0NKWGY0Bs&src=1&schema=8&s=ZjXZZzzTBEW5ZBqc4kCw0kXEd3M</cache_url>
  </result>
...
</results>

위 XML 내용에 대해 자세히 알고 싶다면 구글 데스크탑 검색 API 설명 페이지에서 XML 검색 결과에 대한 설명 부분(http://code.google.com/intl/ko/apis/desktop/docs/queryapi.html#results)을 읽어보기 바란다. 여기에서는 각각 검색된 파일의 위치 및 파일의 내용 일부를 나타내는 url, snippet 항목만을 사용할 것이다.

다시 코드로 돌아와서, 검색 결과를 분석하는 부분을 보면 먼저 XML의 루트 요소(<results>를 나타낸다)에서 count 속성을 읽어와서 검색 결과의 총 갯수를 가져오는 것을 볼 수 있다. 그런 다음. 루트 요소 하위의 요소인 다수의 <result> 요소들을 열거하기 위해 IEnumerator 객체를 얻고, 각 요소를 반복해서 읽어들인 다음 처리한다. 각 <result> 요소에서 <url>과 <snippet> 요소의 내용을 가져온 다음(<snippet> 요소는 경우에 따라서 생략될 경우도 있다), 화면에 표현할 양식을 만들기 위해 GetDisplayItemHTML() 함수를 호출한다.

GetDisplayItemHTML() 함수에서는 url을 처리해서 접근할 수 있는 형태로 만드는데, URI 형태(file://192.168.0.203/c$/test.txt)의 url 주소를 로컬 경로 형태(\\192.168.0.203\c$\test.txt)로 바꾼 후, 해당 파일의 경로, 해당 파일이 포함된 폴더에 대한 링크가 달린 경로명 및 설명을 나타내는 HTML 태그를 생성한다. 중간의 ".zip" 문자열을 찾는 부분은, 구글 데스크탑이 특정 파일이 포함된 zip 압축 파일을 검색해냈을 경우의 처리를 위해 포함된 코드이다. 구글 데스크탑은 압축 파일(아직은 zip 형식만 지원하는 것 같다) 내의 파일을 검색하는 기능도 있으므로, 이 경우에는 압축 파일에 대한 링크를 반환하도록 구현되어 있다. 그 후에 생성된 HTML 태그를 문자열 형태로 반환한다.

이런 처리 과정을 반복해서 검색 결과를 나타내는 HTML을 얻고 난 다음에는, 페이징을 구현한다. num 및 start 인자가 실제로 검색 결과의 갯수(몇 개를 가져오는가, 몇 번째부터 가져오는가)를 나타낸다는 점에 유의해서 코드를 살펴보면 어려운 부분은 없을 것이다.

마지막으로, 처음에 상수로 선언했던 strHtmlTemplate의 대입자에 각각 검색 갯수, 검색 결과, 결과 페이징을 대입해서 divResult.InnerHtml에 할당하여 화면에 출력한다. 그럼 다음과 같은 화면을 볼 수 있다 :


"사용자

그럼 이제 남은 건?
위 검색 결과를 다듬어서 이 포스트의 첫 이미지처럼 꾸미는 일만 남았다.

 

적절한 도구를 모아 하루 동안 노가다를 하면, 파일 서버를 검색하는 사이트 하나를 뚝딱 만들어 낼 수 있습니다. 짠짠.