Service sphinx

From 흡혈양파의 인터넷工房
Revision as of 15:16, 14 August 2015 by Onionmixer (talk | contribs) (형식오류 수정)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
SPHINX 검색엔진 매뉴얼-검색을 위한 sphinx 구성
  • 작성자 : 정재백 (lupfeliz@gmail.com)


개요

SPHINX 검색엔진을 튜닝 (Mongo-db 백엔드 / 아시아 문자 단어단위 색인 등) 및 검색환경에 맞도록 셋팅하는 것을 목표로 한다. 베이스는 sphinx-for-chinese (https://code.google.com/p/sphinx-for-chinese/ ver 2.2.1) 프로젝트명은 sphinx-onion-branch.

설치매뉴얼

소스빌드 준비사항

SPHINX 빌드 이전에 의존 라이브러리 부터 컴파일 해야 한다. Postgresql / Mysql(Mariadb) 과 Mongodb 등은 별도로 설치해야 한다. (바이너리 설치 가능) 설치에 필요한 의존 패키지는 다음과 같다.

패키지명 기타
postgresql 독립운영 / 라이브러리, 색인에 필요한 원본데이터
mongodb 독립운영, 색인시 백엔드 도큐먼트 저장소로 사용
boost 라이브러리, Mongodb 접속 드라이버에 필요
설치에 필요한 의존 패키지들


소스 cloning

소스 리포지터리는 git 을 사용하였으며 다음 커맨드를 사용하여 cloning 이 가능하다.

git clone {{리포지터리경로}}/sphinx_onion_branch


클론된 소스는 다음과 같이 7개의 디렉터리로 이루어져 있다. ( 3개의 소스디렉터리 / 4개의 설정,데이터 디렉터리)

drwxr-xr-x  2 root root   24 Jul 26 18:04 conf
drwxr-xr-x  5 root root   64 Jul 23 09:26 dict
drwxr-xr-x  2 root root   45 Jul 26 18:07 doc
drwxr-xr-x  9 root root 4096 Jul 22 14:31 mongo-c-driver
drwxr-xr-x  6 root root 4096 Jul 22 14:33 mongo-cxx-driver
drwxr-xr-x  5 root root   64 Jul 26 18:05 php
drwxr-xr-x 16 root root 4096 Jul 24 10:00 sphinx


이 중 먼저 mongo-c-driver 를 가장 먼저 컴파일해야 하며, mongo-cxx-driver / sphinx 순으로 컴파일 하도록 한다.


기본적인 설치 위치는 /usr/search 하위에 위치하도록 한다. ( postgresql, mongodb 역시 /usr/search 하위에 설치되어 있다고 가정 )


Mongodb-C-Driver 설치

다음과 같이 컴파일 / 설치를 진행한다. (파란색은 유저 입력)

# cd mongo-c-driver
# ./autogen.sh 
autoreconf-2.69: Entering directory `.'
…
  man                                              : no
  HTML                                             : no
# ./configure --prefix=/usr/search/mongo_driver/
checking for gcc... gcc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
…
  man                                              : no
  HTML                                             : no
# make install
Making install in src/libbson
make[1]: 디렉터리 '/home/assistors/workspace/sphinx_onion_branch/mongo-c-driver/src/libbson' 
…
make[2]: 디렉터리 '/home/assistors/workspace/sphinx_onion_branch/mongo-c-driver' 나감
make[1]: 디렉터리 '/home/assistors/workspace/sphinx_onion_branch/mongo-c-driver' 나감


Mongodb-CXX-Driver 설치

# cd mongo-cxx-driver
# scons --prefix=/usr/search/mongo_driver/ install
scons: Reading SConscript files ...
…
Install file: "build/linux2/normal/mongo/version.h" as "/usr/search/mongo_driver/include/mongo/version.h"
Chmod("/usr/search/mongo_driver/include/mongo/version.h", 0644)
scons: done building targets.


Sphinx 설치

검색엔진을 컴파일하여 설치를 마무리 한다.

# cd sphinx
# ./buildconf.sh
configure.ac:60: warning: AC_LANG_CONFTEST: no AC_LANG_SOURCE call detected in body
…
/usr/share/automake-1.14/am/library.am: archiver requires 'AM_PROG_AR' in 'configure.ac'
src/Makefile.am:6:   while processing library 'libsphinx.a'
# ./configure --prefix=/usr/search/sphinx --without-odbc --without-mysql --with-pgsql --with-pgsql-includes=/usr/search/postgresql/include --with-pgsql-libs=/usr/search/postgresql/lib --with-mongoc --with-mongoc-includes=/usr/search/mongo_driver/include --with-mongoc-libs=/usr/search/mongo_driver/lib 
checking build environment
--------------------------
…
Thank you for choosing Sphinx (onion-branch)
# make install
Making install in libstemmer_c
…
make[1]: 디렉터리 '/home/assistors/workspace/sphinx_onion_branch/sphinx' 나감


기타설정

라이브러리 링크 등을 설정하도록 한다.

# cd /usr/search
# mkdir bin lib
# cd bin
# ln -s ../sphinx/bin/* ./
# cd ../lib
# ln -s ../mongo-driver/lib/*.so ./
# ln -s ../postgresql/lib/*.so ./
# ln -s libpq.so libpq.so.5
# ln -s libmongoc-1.0.so libmongoc-1.0.so.0
# echo $PWD > /etc/ld.so.conf.d/99sphinx.conf
# ldconfig
# ../bin/indexer
Sphinx-for-Chinese 2.2.1-id64-dev (r4311)
…
indexer --all           reindex all indexes defined in 'sphinx.conf'


운영매뉴얼

기본작동방법

스핑크스 검색엔진의 기본 작동 방법을 알아본다.

Indexer

Indexer 는 색인기로서. 데이터소스 입력 (주로 데이터베이스) 을 받아 검색 색인을 작성해 주는 역할을 한다.

색인을 생성하는 방법은 다음과 같다

indexer {컬렉션명} --rotate
1개 컬렉션만 색인할 때
indexer –all --rotate
전체 컬렉션을 색인할 때


대표적인 파라메터 옵션은 다음과 같다.

옵션 설명
--all 전체 컬렉션을 색인한다.
--rotate 기존색인을 백업하고 신규색인으로 로테이션 시킨다.
--conf 설정파일을 지정한다. (기본값은 [설치위치]/etc/sphinx.conf )
Indexer 의 파라메터와 설명


# indexer –-all --rotate
using config file '/usr/search/sphinx/etc/sphinx.conf'...
indexing index 'module_search_total'…
…
total 490 writes, 0.215 sec, 260.8 kb/call avg, 0.4 msec/call avg
rotating indices: successfully sent SIGHUP to searchd (pid=21798).


Searchd

검색엔진 데몬을 띄워준다 기본설정에서 인스턴스는 2개가 뜬다, (daemon, fork)

※ 주의 ! 검색엔진 데몬을 띄우기 이전 Mongodb 가 떠 있는지 먼저 확인해 보도록 한다.
# searchd
using config file '/usr/search/sphinx/etc/sphinx.conf'...
indexing index 'module_search_total'…
…
total 490 writes, 0.215 sec, 260.8 kb/call avg, 0.4 msec/call avg
rotating indices: successfully sent SIGHUP to searchd (pid=21798).
# ps -ef | grep searchd
root     22533     1  0 09:01 ?        00:00:00 ./searchd
root     22534 22533  0 09:01 ?        00:00:01 ./searchd


Mkdict

아시아권 언어의 단어단위 색인을 위해 사전을 생성한다. 사전 생성을 위한 소스는 다음 예시와 같이 준비한다.

# cat words.txt

こね返す
この
このごろ
このほど
このましい
このまま
…
제조자
제조장
제조품
제족
제족기
제졸


사전으로 컴파일하기 위해 다음과 같은 과정을 거친다.

# mkdict words.txt /usr/search/sphinx/dict/system.dict

Preparing...
Making Dictionary:      100% |******************************|
Total words:                    1033231
File size:                      6759424 bytes
Compression ratio:              100 %
Dictionary was successfully created!
※ 현재 기본적으로 들어있는 사전은 한,중,일 기본 사전이 들어있으며, 일본어 형태소 분석기인 메카브 (MeCab) 한글버젼 ( http://www.iamday.net/apps/article/talk/2121/view.iamday )을 사용하여 1차적으로 단어를 정제한 상태임. (사전 출처는 한:은전한닢 기본사전 / 중:Sphinx-for-chinese 기본사전 / 일:MeCab 기본사전)
※ 사전은 DoubleArray 를 이용하며 분석은 단순 Tabular-parsing 을 사용하여, 아시아권 언어 사용상 단어 정제만 잘 해주면 평균정도의 다국어 검색 품질을 기대할 수 있다. (자연어 등은 별도의 해당 국문 분석기를 사용하여야 한다.)


검색설정

기본적인 검색 설정은 /usr/search/sphinx/etc/sphinx.conf 에 들어 있음

검색엔진 동작 포트 설정

검색엔진 동작포트는 sphinx.conf 파일의 searchd 섹션에서 수정이 가능하며 다음과 같다

searchd {
    listen          = 9312
    listen          = 9306:mysql41
    log             = /var/log/sphinx/searchd.log
    query_log       = /var/log/sphinx/query.log
    …
    binlog_path     = /var/lib/sphinx/data
    mysql_version_string = 5.5.21
}
※ sphinx 는 통상적으로 9312 포트를 사용한다. 9306:mysql41 은 mysql 을 emulation 할 때 사용하며 mysql 클라이언트로 접속하여 사용할 수 있다.


Mongodb 통신포트 설정

Mongodb 통신포트는 indexer 와 searchd 에서 둘 다 공통적으로 사용하므로 common 섹션에 위치한다.

common {
    mongo_addr      = localhost:27017
}
※ mongodb 는 도큐먼트 저장소 이므로 가급적 localhost 에 두는 편이 좋다.


색인설정

색인을만들기 위해 도큐먼트 소스를 설정해 주는 부분 sql 및 xml-pipe 등을 사용할 수 있다. 여기서는 sql 을 설명하도록 한다. 색인설정은 구조상 크게 데이터소스 설정과 데이터색인 설정으로 나뉘며, 방법상으로는 전체(Full) 색인과 증분 (Incremental) 색인으로 나뉜다.

구분 설명
구조상 분류 데이터소스 데이터를 가져오는 방법에 대한 설정
데이터색인 데이터를 저장하는 방법에 대한 설정
방법상 분류 전체색인 전체 데이터를 색인하는 방법
증분색인 일정 시점 이후부터의 데이터를 색인하는 방법
색인의 구분 및 설명


클론된 소스 디렉터리의 conf/sphinx.conf 파일에서 볼 수 있다.

※ 주의사항 : sphinx 검색엔진은 반드시 숫자로 구성된 uniq- id 필드를 가지고 있어야 하며, 이는sql 질의 구성시 반드시 맨 처음 필드로 구성 되어야 한다.


주요필드타입 설명
sql_field_string 문자열타입, 형태소분석 수행, 정렬 가능 몽고DB 저장
sql_mongo_string 문자열타입, 형태소분석 수행, 정렬 및 필터링 사용 불가능 몽고DB 저장
sql_attr_string 문자열타입, 형태소분석하지 않음. 정렬 및 필터링사용가능 몽고DB 미지원
sql_attr_uint 정수형, 형태소분석 하지 않음. 정렬 및 필터링 사용 가능 몽고DB 미지원
sql_attr_float 실수형, 형태소분석 하지 않음. 정렬 및 필터링 사용 가능 몽고DB 미지원
sql_attr_bigint 큰정수형, 형태소분석 하지 않음. 정렬 및 필터링 사용 가능 몽고DB 미지원
sql_attr_timestamp 날자형, 형태소분석 하지 않음. 정렬 및 필터링 사용 가능 몽고DB 미지원
sql_attr_bool 불리언형, 형태소분석 하지 않음. 정렬 및 필터링 사용 가능 몽고DB 미지원
주요 필드 타입 및 필드에 대한 설명


Full-indexing (전체) 설정

데이터베이스 전체를 덤프 하도록 하며 이 때 걸리는 데이터베이스 부하는 감안 해야 한다. (보통은 6시간 간격 내지는 하루 한 번 전체색인을 하며, 데이터 량에 따라 수개월만에 전체색인을 하는곳도 있음)

lookandwalk 의 경우 1개의 소스로 모두 처리하는 반면, 색인되는 개개별의 스키마가 모두 다른 경우 혹은 전략적으로 다르게 색인하여 사용할 수 있다. (일단 요청에 의해 한개의 색인으로 처리함)


데이터 소스
source module_search_total {
    type            = pgsql
    sql_host        = 192.168.0.21
    sql_user        = lookandwalkdump
    sql_pass        = 2****y
    sql_db          = lookandwalkbeta
    sql_port        = 5432
    sql_sock        =
    sql_query = \
        SELECT \
            '11'||bbs.seq AS uid,\
            'notice' AS category,\
            bbs.service_notice_write_lang AS lang,\
            bbs.service_notice_subject AS subject,\
            bbs.service_notice_subject AS subject_f,\
            bbs.service_notice_text AS text,\
            admin.admin_name AS user,\
            extract(epoch from bbs.service_notice_date_write) AS date,\
            '' AS tag \
        FROM system_administrator admin, module_service_notice bbs \
        WHERE bbs.service_notice_writer = admin.seq \
        UNION \
        SELECT \
            '12'||bbs.seq AS uid,\
            'story' AS category,\
            bbs.story_write_lang AS lang,\
            bbs.story_subject AS subject,\
            bbs.story_subject AS subject_f,\
            bbs.story_text AS text,\
            suser.user_id AS user,\
            extract(epoch from bbs.story_date_write) AS date,\
            story_tag AS tag \
        FROM system_user suser,module_story_data bbs \
        WHERE bbs.story_writer = suser.seq \
        UNION \
        SELECT \
            '13'||bbs.seq AS uid,\
            'qna' AS category,\
            bbs.serviceqna_write_lang AS lang,\
            bbs.serviceqna_subject AS subject,\
            bbs.serviceqna_subject AS subject_f,\
            bbs.serviceqna_text AS text,\
            suser.user_id AS user,\
            extract(epoch from bbs.serviceqna_date) AS date,\
            '' AS tag \
        FROM system_user suser,module_serviceqna_data bbs \
        WHERE bbs.serviceqna_writer = suser.seq \
        UNION \
        SELECT \
            '14'||bbs.seq AS uid,\
            'faq' AS category,\
            bbs.servicefaq_write_lang AS lang,\
            bbs.servicefaq_subject AS subject,\
            bbs.servicefaq_subject AS subject_f,\
            bbs.servicefaq_text AS text,\
            suser.user_id AS user,\
            extract(epoch from bbs.servicefaq_date) AS date,\
            '' AS tag \
        FROM system_user suser,module_servicefaq_data bbs \
        WHERE bbs.servicefaq_writer = suser.seq \
        UNION \
        SELECT \
            '15'||bbs.seq AS uid,\
            'accus' AS category,\
            bbs.accusation_write_lang AS lang,\
            bbs.accusation_subject AS subject,\
            bbs.accusation_subject AS subject_f,\
            bbs.accusation_text AS text,\
            suser.user_name AS user,\
            extract(epoch from bbs.accusation_date_notice) AS date,\
            '' AS tag \
        FROM system_user suser,module_accusation_data bbs \
        WHERE bbs.accusation_writer = suser.seq \
        UNION \
        SELECT \
            '16'||bbs.seq AS uid,\
            'blog' AS category,\
            bbs.blog_write_lang AS lang,\
            bbs.blog_subject AS subject,\
            bbs.blog_subject AS subject_f,\
            bbs.blog_text AS text,\
            suser.user_id AS user,\
            extract(epoch from bbs.blog_date_modify) AS date,\
            blog_tag AS tag \
        FROM system_user suser, module_blog_data bbs, module_blog_category AS bbs_category \
        WHERE bbs.blog_writer = suser.seq AND bbs.blog_category_seq = bbs_category.seq AND bbs_category.category_show = 0 \

    sql_mongo_string    = category
    sql_mongo_string    = lang
    sql_field_string    = subject
    sql_mongo_string    = text
    sql_mongo_string    = user
    sql_attr_timestamp  = date
    sql_mongo_string    = tag
    sql_ranged_throttle = 0
}
데이터 색인
index module_search_total {
    source          = module_search_total
    path            = /usr/search/sphinx/data/module_search_total
    docinfo         = extern

    charset_type    = utf-8
    chinese_dictionary = /usr/search/sphinx/dict/system.dict
    wordforms       = /usr/search/sphinx/dict/wordforms.txt
    charset_table   = 0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F
    morphology      = none
    min_word_len    = 0
    html_strip      = 1
    html_index_attrs = img=alt,title;a=title
    html_remove_elements = style,script
    expand_keywords = 1
}
Full-indexing 에 대한 설정


Incremental-Indexing (증분) 설정

증분색인은 전체색인과 동일하나 데이터를 가져오는 방법 (마지막 색인 이후 데이터만 추출)과 경로 만이 다를 뿐이다. 예제에서 같은 부분은 회색으로 처리하였으며, 다른부분은 빨간색으로 처리 하였다.

※ 증분색인은 데이터 갱신주기에 따라 다를 수 있으며, 보통 5분주기로 갱신한다. (데이터 변화에 민감한 곳은 1분에 한번, 혹은 초단위 갱신을 하는곳도 있으며, 데이터베이스 갱신 트리거를 외부 프로세스로 걸 수 있는 경우 그러한 방식으로 증분을 하는 경우도 있음)


데이터 소스
source module_search_total_inc {
    type            = pgsql
    sql_host        = 192.168.0.21
    sql_user        = lookandwalkdump
    sql_pass        = 2****y
    sql_db          = lookandwalkbeta
    sql_port        = 5432
    sql_sock        =
    sql_query = \
        SELECT \
            '11'||bbs.seq AS uid,\
            'notice' AS category,\
            bbs.service_notice_write_lang AS lang,\
            bbs.service_notice_subject AS subject,\
            bbs.service_notice_subject AS subject_f,\
            bbs.service_notice_text AS text,\
            admin.admin_name AS user,\
            extract(epoch from bbs.service_notice_date_write) AS date,\
            '' AS tag \
        FROM system_administrator admin, module_service_notice bbs \
        WHERE bbs.service_notice_writer = admin.seq \
        AND bbs.service_notice_date_write > to_timestamp('2015-07-27 00:00:00','YYYY-MM-DD HH24:MI:SS') \
        UNION \
        SELECT \
            '12'||bbs.seq AS uid,\
            'story' AS category,\
            bbs.story_write_lang AS lang,\
            bbs.story_subject AS subject,\
            bbs.story_subject AS subject_f,\
            bbs.story_text AS text,\
            suser.user_id AS user,\
            extract(epoch from bbs.story_date_write) AS date,\
            story_tag AS tag \
        FROM system_user suser,module_story_data bbs \
        WHERE bbs.story_writer = suser.seq \
        AND bbs.story_date_write > to_timestamp('2015-07-27 00:00:00','YYYY-MM-DD HH24:MI:SS') \
        UNION \
        SELECT \
            '13'||bbs.seq AS uid,\
            'qna' AS category,\
            bbs.serviceqna_write_lang AS lang,\
            bbs.serviceqna_subject AS subject,\
            bbs.serviceqna_subject AS subject_f,\
            bbs.serviceqna_text AS text,\
            suser.user_id AS user,\
            extract(epoch from bbs.serviceqna_date) AS date,\
            '' AS tag \
        FROM system_user suser,module_serviceqna_data bbs \
        WHERE bbs.serviceqna_writer = suser.seq \
        AND bbs.serviceqna_date > to_timestamp('2015-07-27 00:00:00','YYYY-MM-DD HH24:MI:SS') \
        UNION \
        SELECT \
            '14'||bbs.seq AS uid,\
            'faq' AS category,\
            bbs.servicefaq_write_lang AS lang,\
            bbs.servicefaq_subject AS subject,\
            bbs.servicefaq_subject AS subject_f,\
            bbs.servicefaq_text AS text,\
            suser.user_id AS user,\
            extract(epoch from bbs.servicefaq_date) AS date,\
            '' AS tag \
        FROM system_user suser,module_servicefaq_data bbs \
        WHERE bbs.servicefaq_writer = suser.seq \
        AND bbs.servicefaq_date > to_timestamp('2015-07-27 00:00:00','YYYY-MM-DD HH24:MI:SS') \
        UNION \
        SELECT \
            '15'||bbs.seq AS uid,\
            'accus' AS category,\
            bbs.accusation_write_lang AS lang,\
            bbs.accusation_subject AS subject,\
            bbs.accusation_subject AS subject_f,\
            bbs.accusation_text AS text,\
            suser.user_name AS user,\
            extract(epoch from bbs.accusation_date_notice) AS date,\
            '' AS tag \
        FROM system_user suser,module_accusation_data bbs \
        WHERE bbs.accusation_writer = suser.seq \
        AND bbs.accusation_date_notice > to_timestamp('2015-07-27 00:00:00','YYYY-MM-DD HH24:MI:SS') \
        UNION \
        SELECT \
            '16'||bbs.seq AS uid,\
            'blog' AS category,\
            bbs.blog_write_lang AS lang,\
            bbs.blog_subject AS subject,\
            bbs.blog_subject AS subject_f,\
            bbs.blog_text AS text,\
            suser.user_id AS user,\
            extract(epoch from bbs.blog_date_modify) AS date,\
            blog_tag AS tag \
        FROM system_user suser, module_blog_data bbs, module_blog_category AS bbs_category \
        WHERE bbs.blog_writer = suser.seq AND bbs.blog_category_seq = bbs_category.seq AND bbs_category.category_show = 0 \
        AND bbs.blog_date_modify > to_timestamp('2015-07-27 00:00:00','YYYY-MM-DD HH24:MI:SS') \

    sql_mongo_string    = category
    sql_mongo_string    = lang
    sql_field_string    = subject
    sql_mongo_string    = text
    sql_mongo_string    = user
    sql_attr_timestamp  = date
    sql_mongo_string    = tag
    sql_ranged_throttle = 0
}
데이터 색인
index module_search_total_inc {
    source          = module_search_total_inc
    path            = /usr/search/sphinx/data/module_search_total_inc
    docinfo         = extern

    charset_type    = utf-8
    chinese_dictionary = /usr/search/sphinx/dict/system.dict
    wordforms       = /usr/search/sphinx/dict/wordforms.txt
    charset_table   = 0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F
    morphology      = none
    min_word_len    = 0
    html_strip      = 1
    html_index_attrs = img=alt,title;a=title
    html_remove_elements = style,script
    expand_keywords = 1
}
Incremental-Indexing 에 대한 설정


기타설정

위에서 보는바와 같이 전체색인과 증분색인은 대동소이 하므로 템플릿으로 동일부분을 작성하고 쉘스크립트로 증분시간만 조작하여 병합하여 사용하도록 한다. 아래는 mk_index.sh 예시

mk_index.sh
#!/bin/bash
SPHINX_ROOT="/usr/search/sphinx"
#현재시각
TIME_STR=`date +"%Y-%m-%e %H:%M:%S"`
#시간저장경로
TIME_PATH="$SPHINX_ROOT/etc/last_index"
#마지막 갱신시간
LAST_TIME_STR=`cat $TIME_PATH` 2>/dev/null
if [[ $LAST_TIME_STR = '' ]]; then
    LAST_TIME_STR="0000-00-00 00:00:00";
fi
#갱신시간이 없으면 최초시간으로 
if [[ $1 = '-f' ]]; then
    LAST_TIME_STR="0000-00-00 00:00:00";
fi
echo $LAST_TIME_STR
#현재시간을 다음번에 사용하기 위해 저장 
echo $TIME_STR > $TIME_PATH
echo "LAST TIME : $LAST_TIME_STR"
#템플릿경로
TEMPLATE=$SPHINX_ROOT/etc/sphinx.conf.template
#공통설정
COMMONS=$SPHINX_ROOT/etc/sphinx.conf.commons
CONF1=$SPHINX_ROOT/etc/sphinx1.conf
CONF2=$SPHINX_ROOT/etc/sphinx2.conf
CONF=$SPHINX_ROOT/etc/sphinx.conf
#전체색인 설정 생성
cp $TEMPLATE $CONF1
perl -p -i -e "s/\{\{LAST_TIME\}\}/0000-00-00 00:00:00/g" $CONF1
perl -p -i -e "s/\{\{SUFFIX\}\}//g" $CONF1
#증분색인 설정 생성
cp $TEMPLATE $CONF2
perl -p -i -e "s/\{\{LAST_TIME\}\}/$LAST_TIME_STR/g" $CONF2
perl -p -i -e "s/\{\{SUFFIX\}\}/_inc/g" $CONF2
#설정 합치기
cat $CONF1 > $CONF
cat $CONF2 >> $CONF
cat $COMMONS >> $CONF
#임시설정파일 삭제
rm -f $CONF1 $CONF2
if [[ $1 = '-f' ]]; then
#전체색인
    $SPHINX_ROOT/bin/indexer module_search_total --rotate
else 
#증분색인
    $SPHINX_ROOT/bin/indexer module_search_total_inc --rotate
    $SPHINX_ROOT/bin/indexer --merge  module_search_total module_search_total_inc --rotate
fi
#필요없는 증분색인 삭제
rm -f $SPHINX_ROOT/data/module_search_total_inc.*
※ crontab 의 설정 예
*/5 * * * * /usr/search/bin/mk_index.sh
0 * * * * /usr/search/bin/mk_index.sh -f


사전구축 전략

사전은 검색엔진이 색인 및 검색을 할 때 기초가 되는 키워드를 분해하는 단위로서 Tabular parsing 의 특성을 잘 알아야 사전 구축 전략을 잘 세울 수 있다.

단어의 추가 방법

Tabular parsing 은 타 형태소 분석 알고리즘 보다는 유연하지만 특성은 단어 길이가 길수록 매칭에 높은 점수를 받는다는 특성은 타 분석 알고리즘 과 크게 다르지 않다. 따라서 "서울" 이라는 단어와 "서울시청" 이라는 단어가 사전에 등록되어 있는 경우 "서울시청" 이라는 단어가 문서에서 발견되면 "서울" +"시청" 으로 분해되는것이 아니라 "서울시청" 한단어로 분해된다. 이렇게 분해된 경우 "서울" 이라는 단어로 검색하면 검색결과가 나오지 않는다. 따라서 복합어 등은 전부 풀어서 등록해야 한다. (다른방법으로는 가중치를 주는 방법도 있으나 이는 전문적인 사전 관리도구 / 관리자가 필요하다)

※ 단 전략적으로 복합어가 검색되어야 하는 경우 일부러 복합어를 사용하기도 한다. 예를들어 "노트북"의 경우 "노트" 와 "북" 의 복합어로 볼 수 있겠으나 통상 인식 상 "노트북" 과 "노트"+"북" 은 별개의 것으로 생각하며, 검색빈도 역시 확연한 차이가 있으므로 이런 경우 둘 다 등록해 주는것이 맞다.
※ 1글자단어는 검색 품질에 큰 영향을 미친다. 예를 들어 "꽃" "놀" "이" 라는 각각의 3글자가 사전에 등록되어있는 경우 "꽃놀이" 라는 단어도 등록 되지만 "이꽃놀" 이라는 무의미한 구간 혹은 "아놀드는꽃과사람들이" 와 같이 띄어진 구간도 검색되게 되므로 가급적 1글자 단어는 넣지 않도록 한다.
※보통 한글의 경우 2~3 글자 단어가 주를 이루며 최장 7자 정도 까지 등록한다.


단어 정제 방법

이미 등록된 단어의 경우 정제하기가 쉽지가 않다. 이 경우 기 마련된 형태소 분석기가 있는경우 매우 유용하게 사용될 수 있다. 국문/일문의 경우 메카브(Mecab) 를 권장한다.

※ 메카브의 경우 사전만 구할수 있다면 아시아권 언어를 모두 사용 가능하다. 다음 예제와 같이 복합어 들을 모두 걸러내 주고, 품사구분까지 해 준다. (검색엔진에서는 일반적으로 명사 – NNG/NNP 태그 - 가 주로 사용되며, 조사 -EF/EP- 등은 되도록 색인하지 않는다. Compound 는 복합명사로 분해된 단어들을 같이 보여준다.)


아래 예시화면 에서 쉘스크립트 등을 사용하여 명사를 추출한다.

Mecab 이용 방법
# cat korean.txt
안녕하세요
손톱깎이
경포대
부산해운대
경주첨성대

# cat korean.txt | ./mecab
안녕    NNG,*,T,안녕,*,*,*,*
하      XSV,*,F,하,*,*,*,*
세요    EP+EF,*,F,세요,Inflect,EP,EF,시/EP/*+어요/EF/*
EOS
손톱깎이        NNG,*,F,손톱깎이,Compound,*,*,손톱/NNG/*+깎이/NNG/*
EOS
경포대  NNG,*,F,경포대,Compound,*,*,경포/NNG/*+대/NNG/*
EOS
부산    NNP,지명,T,부산,*,*,*,*
해운대  NNG,*,F,해운대,Compound,*,*,해운/NNG/*+대/NNG/*
EOS
경주    NNP,지명,F,경주,*,*,*,*
첨성대  NNG,*,F,첨성대,Compound,*,*,첨성/NNG/*+대/NNG/*
EOS
※ 형태소 분석기가 없는 경우는 필요한 단어 (단어별로 떨어져 있는 키워드) 부터 채워 나가는 방식을 사용한다.
※ 사전생성 시 ( ) @ 등의 단독 문자들은 검색 예약문자 이기 때문에 절대 들어가서는 안된다.
※ 사전을 새로 만들고 나서는 반드시 검색 데몬을 재시작 해 주어야 한다. ( killall searchd && ./searchd )


동의어 사전 등록 및 활용

동의어는 sphinx.conf 파일 중 wordforms 항목에서 설정 가능하며 다음과 같은 형식으로 작성한다.

Wordforms.txt
동의어 > 대표단어


예를 들면 "c2d" 를 검색했을 때 "Core 2 Duo" 가 검색되길 바란다면 wordforms.txt 에 다음과 같이 기재한다.

c2d > Core 2 Duo


이후 c2d 를 검색하면 Core 2 Duo 로 치환되어 검색되어 진다.


검색구축전략

검색은 데이터베이스와 다르게 최대한 많은양의 정보를 담아두는 편이 좋다. (한번의 검색으로 모든 데이터를 가져올 수 있도록) 단. 그 안에서 용량은 작게 만들 필요가 있으므로 검색 구축 전략이 필요하다.

데이터베이스와 다른점

검색과 데이터베이스의 다른점은 다음과 같다.

  1. 한번의 검색으로 검색된 전체 레코드 갯수를 알 수 있다. (db 에서는 갯수조회 1번, 데이터조회 1번 총 2번의 연산 소요)
  2. 데이터베이스처럼 레코드 간 연산이 유연하지 못하다. (Join 등이 되지 않기 때문에 연산은 한 컬렉션 내에서만 한정)
  3. Join 등 다중 테이블 연산이 불가능하다. (따라서 색인시 필요한 모든 조인을 수행한 후 색인한다)
  4. 색인은 데이터베이스로 표현하자면 view 에 많이 가깝다.
  5. 그룹핑이 레코드를 결정하지 않는다. (그룹핑은 현재 검색 결과 내에서 그룹 연산을 시도한다 / 예를들어 쇼핑몰의 경우 아이폰 검색어가 각 카테고리별로 몇 개가 계산되었는가 등을 db 에서 표현하려면 각 카테고리마다 연산을 해야 하지만, 검색엔진은 카테고리로 그룹핑 해 놓으면 카운팅 연산 한번으로 처리한다.)


스키마 통일

데이터베이스는 필요에 따라 많은 양의 필드들이 소요되지만 검색에서는 일반적으로 그다지 많은 정보가 필요하지 않다. 대표적으로 다음 필드들로 묶어 놓으면 검색에는 큰 지장없다.

※ 출력할 필드와 연산할 필드(정렬, 필터링) 를 추출한다.
항목 설명 예시 비고
카테고리 검색된 항목의 카테고리 category
고유번호 검색된 항목의 고유번호 uid ※고유번호는 항상 고유한 숫자(Integer/long)여야 한다.
여러개 게시판이 합쳐진 경우 앞자리 숫자를 다르게 하여 표현 가능
제목 검색되었을 때 표시될 제목 Subject(글제목), title, name (항목명)...
내용 검색된 항목의 세부내용 Text, contents, ...
태그 태그내용 tags
글쓴이 글쓴이 Author, writer, ...
날자 작성 / 갱신일자 DateWrite, rdate, ... 정렬의 기준이 될 수 있음.


검색필드 및 필터속성 지정

검색필드

색인필드는 검색대상이 되는 필드로서 형태소 분석이 이루어진다. 기본적으로 sphinx 에서 field 로 잡게 되면 형태소분석을 통해 검색필드로 지정된다. ( 스키마 생성시 sql_field_string / sql_mongo_string 지정 )


필터속성

필터필드는 정렬, 필터링 (키가 170이상 180 미만인 사람을 출력하라 등) 에 사용되는 칼럼으로 sphinx 에서 attr 로 잡게 되면 필터링 목적으로 사용할 수 있다. 주로 숫자(정수,소숫점,날자 / sql_attr_uint, sql_attr_timestamp ) 등에 사용되며, 문자열은 sql_attr_string 등을 사용할 수 있다. (sql_attr_string 으로 지정된 칼럼은 형태소분석을 하지 않으므로 주의)



트러블슈팅 및 주의사항

색인 / 검색간 발생가능한 오류와 이에대한 대처방안 모색

색인간 오류 발생 사항 및 대처방안

증상 설명 및 대처방안 비고
error while loading shared libraries 라이브러리 로드가 되지 않음.
2.1.5 기타설정 을 참고하여 라이브러리 패스 설정
ldd indexer 를 수행하여 누락된 라이브러리 점검
오류
terminate called after throwing an instance of 'mongo::ConnectException' mongodb 에 접속할 수 없어 발생하는 오류
mongodb 를 재시작 하거나 mongodb가 다른 서버에 존재하는 경우 방화벽 등을 재확인 하도록 한다.
오류
FATAL: failed to open … 용량이 부족하거나 파일 권한이 없어 발생.
충분한 용량이 있는지, 데이터 경로에 쓰기 권한이 있는지 확인한다.
오류
WARNING: failed to open pid_file '/var/run/sphinx/searchd.pid'. 검색엔진이 시작되지 않아 재시작할 데몬이 없는경우 발생 경고
WARNING: indices NOT rotated. --rotate 시 이전색인이 없어 rotation 할 수 없는경우 발생 경고


검색간 오류 발생 사항 및 대처방안

증상 설명 및 대처방안 비고
error while loading shared libraries 라이브러리 로드가 되지 않음.
2.1.5 기타설정 을 참고하여 라이브러리 패스 설정
ldd indexer 를 수행하여 누락된 라이브러리 점검
오류
terminate called after throwing an instance of 'mongo::ConnectException' mongodb 에 접속할 수 없어 발생하는 오류
mongodb 를 재시작 하거나 mongodb가 다른 서버에 존재하는 경우 방화벽 등을 재확인 하도록 한다.
오류
간헐적으로 검색엔진이 멎은 후 응답이 없는경우 Searchd 포크 프로세스가 죽은 경우 발생, 원본은 살아있기 때문에 재시작 되지만 해당 검색 스레드는 멎은것 처럼 보임.
주로 메모리 누수 현상으로 인한 segment fault 가 원인일 수 있음.
증상 발견시 소스분석하여 메모리 누수를 찾아야 함.
오류
어떤 검색어를 넣어도 결과값이 0인경우 사전 생성시 ( ) @ 등의 단독문자가 있는경우 쿼리 자체를 단어로 인식하여 생기는 오류, 사전에서 ( ) @ 등의 단독문자를 제거하여 재생성 후 검색데몬을 다시 시작하면 된다. 데이터오류


검색페이지소스 (php)

기본 검색 개요

최초 유저가 웹서버에 요청시

  1. search.php 를 통해 요청을 받고
  2. library.php 를 이용해 검색식을 조합한 후
  3. sphinxapi.php 를 통해 검색엔진과 소통하여 데이터를 받아온다.
  4. 마지막으로 search_item_xxx.php 를 통해 html 형태로 보여진다.

기본 검색 개요


검색 라이브러리 설명

Library.php

Sphinx.php 를 사용하여 검색엔진과 소통하는것을 사용하기 쉽게 wrapping 한 클래스

클래스 메서드 비고
Searcher search 검색메서드 키워드, 필터링, 정렬조건과 페이징용 오프셋, 결과갯수 등을 파라메터로 받아 조합하여 검색엔진에 질의한다.
setTags 하이라이팅에 사용될 태그쌍 (기본값은 )
highlight 검색엔진에 본문과 키워드를 넘기고 하이라이팅/요약된 본문을 받는다.
PageNavigator setTotal 전체 레코드 갯수를 입력한다.
getRows 레코드 갯수를 반환한다.
startPage 현재 페이지를 포함하는 네비게이션 시작페이지 번호를 계산
endPage 현재 페이지를 포함하는 네비게이션 종료페이지 번호를 계산
prevPage 이전페이지 계산
nextPage 다음페이지 계산


Sphinx.php

소켓통신을 사용하여 검색엔진과 직접 소통하는 라이브러리, sphinx의 api 디렉터리에 존재한다.

클래스 주요메서드 비고
SphinxClient Query 검색엔진에 질의하는 메서드
SetSelect 질의시 가져올 칼럼 정의
SetServer 검색엔진 주소 / 포트 정의
SetArrayResult Php 에서 array 로 형변환을 거칠것인지 결정
SetMatchMode Matchmode 결정 (주로 SPH_MATCH_EXTENDED2 사용)
ResetFilters 필터 초기화
SetFilterRange 범위필터 설정 ( 100이상 1000이하 등)
SetFilterString 검색결과 중 특정 단어만 검색하려 할 경우
SetGroupBy 그룹연산을 위한 필드를 정의 ( attr 필드만 가능 )
SetLimits 레코드의 시작위치와 갯수 정의
BuildExcerpts 하이라이팅 (Snippet)
SetConnectTime 접속시간 설정
SetMaxQueryTime 최대 질의시간 설정


페이지 세부 소스

Search.php (검색 핵심 부분 일부 발췌)

<?php
//라이브러리 임포트
require_once ("../share/library.php");
//각종 파라메터 초기화
$keyword = @$_POST["keyword"];
$search_category = @$_POST["search_category"];
$cpage = @$_POST["cpage"] * 1;
$sortby = @$_POST["sortby"];
$searchby = @$_POST["searchby"];
$interval = @$_POST["interval"];
if(!$search_category) {
    $search_category = "all";
}
if($cpage < 1) { 
    $cpage = 1;
}
if($sortby=="") {
    $sortby="rank";
}
if($searchby=="") {
    $searchby="all";
}
if($interval=="") {
    $interval="all";
}
//검색카테고리 설정
$collections = array(
    "all"       => array("통합검색",    "../search/search_item_all.php"),
    "notice"    => array("공지사항",    "../search/search_item_notice.php"),
    "story"     => array("나침반캠프",  "../search/search_item_story.php"),
    "qna"       => array("서비스Q&A",   "../search/search_item_qna.php"),
    "faq"       => array("서비스FAQ",   "../search/search_item_faq.php"),
    "accus"     => array("신고게시판",  "../search/search_item_accus.php"),
    "blog"      => array("여행정보",    "../search/search_item_blog.php")
);

$total_time = 0.0;
//페이지 네비게이터, 페이지 바운더리와 레코드 바운더리 계산용
$pnav = new PageNavigator(20, 10); 
$results = array();
//검색 시작
$searcher = new Searcher();
$searcher->setTags(array("<span class=\"keyword\">","</span>"));
foreach ($collections as $collection=>$v) {
    if($search_category == $collection) {
        //페이지네이션이 필요한 경우 (주검색, 결과까지 리턴)
        $results[$collection] = $searcher->search($collection, $keyword, $searchby, $interval, $sortby, $pnav->getRows() * ($cpage - 1), $pnav->getRows());
    } else {
        //검색 총 갯수만 필요한 경우 (부가검색, 결과는 필요없고 검색 총갯수만 사용)
        $results[$collection] = $searcher->search($collection, $keyword, $searchby, $interval, $sortby, 0, 0); 
    }   
    $total_time+= @$results[$collection]["time"];
}


search_item_xxx.php (출력 핵심부분 일부 발췌)

<h4><?=$collection_data[0]?> / 검색어 "<span class="keyword"><?=$keyword?></span>" 에 대해 총 ( <?=$result["total"]?> ) 건의 자료가 검색되었습니다. (총검색소요시간 : <?=$total_time?>초)</h4>
<?php
//검색자료 탐색
for ( $inx=0 ; $inx < count($docinfo); $inx++) {  
    $record = $docinfo[$inx]["attrs"];
    $category = $record["category"];

    $record["tag"] = "<a>".preg_replace("/[|,]+/i","</a> <a>",$record["tag"])."</a>";
    $lang = $record["lang"];
    $uid = substr($docinfo[$inx]["id"],2); //앞 두자리는 구분자

    $link = "";
    //각 링크에 대한 바로가기 조합
    if($category == "notice") {
        $link = "http://www.lookandwalk.com/".$lang."/ccenter/notice/noticeview/".$uid;
    } else if($category == "story") {
        $link = "http://www.lookandwalk.com/".$lang."/community/review/review_view/".$uid;
    } else if($category == "qna") {
        $link = "http://www.lookandwalk.com/".$lang."/ccenter/qna/qnaview/".$uid;
    } else if($category == "faq") {
        $link = "http://www.lookandwalk.com/".$lang."/ccenter/faq/faqview/".$uid;
    } else if($category == "accus") {
        //$link = "http://blog.lookandwalk.com/".$lang."/community/info/info_view/".$uid;
    } else if($category == "blog") {
        $link = "http://blog.lookandwalk.com/".$lang."/community/info/info_view/".$uid;
    }  
?>


소스 핵심 변경사항

각종 컨피그 파일들

acinclude.m4 / AC_CHECK_MONGOC 추가
dnl ---------------------------------------------------------------------------
dnl Macro: AC_CHECK_MONGOC
dnl First check for custom MongoC paths in --with-mongoc-* options.
dnl ---------------------------------------------------------------------------

AC_DEFUN([AC_CHECK_MONGOC],[

# Check for custom includes path
if test [ -z "$ac_cv_mongoc_includes" ] 
then 
    AC_ARG_WITH([mongoc-includes], 
        AC_HELP_STRING([--with-mongoc-includes], [path to MongoC header files]),
        [ac_cv_mongoc_includes=$withval])
fi
if test [ -n "$ac_cv_mongoc_includes" ]
then
    AC_CACHE_CHECK([MongoC includes], [ac_cv_mongoc_includes], [ac_cv_mongoc_includes=""])
    MONGOC_CFLAGS="-I$ac_cv_mongoc_includes/libmongoc-1.0 -I$ac_cv_mongoc_includes/libbson-1.0 -I$ac_cv_mongoc_includes/ "
fi

# Check for custom library path
if test [ -z "$ac_cv_mongoc_libs" ]
then
    AC_ARG_WITH([mongoc-libs], 
        AC_HELP_STRING([--with-mongoc-libs], [path to MongoC libraries]),
        [ac_cv_mongoc_libs=$withval])
fi
if test [ -n "$ac_cv_mongoc_libs" ]
then
    AC_CACHE_CHECK([MongoC libraries], [ac_cv_mongoc_libs], [ac_cv_mongoc_libs=""])
    MONGOC_PKGLIBDIR="$ac_cv_mongoc_libs"
    MONGOC_LIBS="-L$MONGOC_PKGLIBDIR -pthread -lmongoc-1.0 -lbson-1.0 -lmongoclient -lboost_system -lboost_thread-mt -lboost_system -lboost_regex "
fi
])
configure.ac / 컴파일 옵션선택 부분 추가
# check if we should compile with MongoC support
AC_ARG_WITH([mongoc],
            AC_HELP_STRING([--with-mongoc], [compile with MongoC support (default is disabled)]),
            [ac_cv_use_mongoc=$withval], [ac_cv_use_mongoc=no]
)
AC_MSG_CHECKING([whether to compile with MongoC support])
if test x$ac_cv_use_mongoc != xno; then
    AC_CHECK_MONGOC([ac_cv_use_mongoc])
    AC_DEFINE(USE_MONGOC,1,[Define to 1 if you want to compile with MongoC support])
    AC_SUBST([MONGOC_LIBS])
    AC_SUBST([MONGOC_CFLAGS])
else
    AC_MSG_RESULT([no])
fi
AM_CONDITIONAL(USE_MONGOC, test x$ac_cv_use_mongoc != xno )


sphinx.h / sphinx.cpp

sphinx.h / 추가분
...

#if USE_MONGOC
//mongodb includes
#include "mongo/bson/bson.h"
#include "mongo/client/dbclient.h"
using mongo::BSONElement;
using mongo::BSONObj;
using mongo::BSONObjBuilder;
using mongo::DBClientCursor;
#endif

...

extern CSphString g_sMongoAddr; //mongodb address

...
sphinx.cpp / 추가분
...

#if USE_MONGOC
CSphString          g_sMongoAddr            = SHAREDIR;
#endif

//전역변수 선언부 g_sMongoAddr 은 공통 스키마에서 읽어들인 후 전역변수로 선언된다.
...
//----------------------------------------색인 생성 부
int CSphIndex_VLN::Build ( const CSphVector<CSphSource*> & dSources, int iMemoryLimit, int iWriteBuffer )

...

//mongo driver 초기화. 
#if USE_MONGOC
mongo::DBClientConnection mongoConn;
mongoConn.connect(g_sMongoAddr.cstr());
char collectionName[strlen(m_tSchema.m_sName.cstr()) + 7];
{
    sprintf(collectionName,"sphinx.%s\0",m_tSchema.m_sName.cstr());
}
char* idstr = (char*)malloc(100);
#endif

...

//mongodb 조건문 설정, 문서 id
#if USE_MONGOC
sprintf(idstr,"%ld",pSource->m_tDocInfo.m_iDocID);
BSONObjBuilder mongoDoc;
#endif // USE_MONGOC

...

//mongodb 도큐먼트 생성 ( 각 칼럼 입력 )
#if USE_MONGOC
const CSphColumnInfo & tCol = m_tSchema.GetAttr(dStringAttrs[i]);
mongoDoc << tCol.m_sName.cstr() << sData;
#endif // USE_MONGOC
...
#ifdef USE_MONGOC   
                    if(tCol.m_bStore) {
#endif
...
#ifdef USE_MONGOC   
                    }
#endif

...

//mongodb 에 완성된 도큐먼트 업서트 (update - insert)
#if USE_MONGOC
            mongoConn.update (collectionName, MONGO_QUERY("_id" << idstr), mongoDoc.obj(), true, false);
#endif // USE_MONGOC

...

//할당된 메모리를 해제한다.
#if USE_MONGOC
free(idstr);
#endif

//----------------------------------------색인 생성 부 종료


searchd.cpp / indexer.cpp

Searchd.cpp (검색데몬) 에서는 크게 3개 부분에서 mongodb 조회가 사용된다. 1) CalcResultLength 함수 (네트워크에 보내기 전 본문길이를 계산함)와 2) SendResult 함수 (직접적으로 네트워크에 결과를 보내는 함수) 에서 사용되며, Spinxql 을 사용하는 경우 3) SendMysqlSelectResult 함수 (유사 Mysql Query 를 사용) 에서 사용된다.

searchd.cpp / 추가분
...

//----------------------------------------본문길이측정부
int CalcResultLength (CSphString m_sName, int iVer, const CSphQueryResult * pRes, const CSphTaggedVector & dTag2Pools, bool bAgentMode, const CSphQuery & tQuery, int iMasterVer , std::map<int, void*>m_mContext )
{

...

    if ( iVer>=0x117 && dStringItems.GetLength() )
    {
//mongo driver 초기화
#if USE_MONGOC
    mongo::DBClientConnection mongoConn;
    mongoConn.connect(g_sMongoAddr.cstr());
    char collectionName[strlen(m_sName.cstr()) + 7];
    {
        sprintf(collectionName,"sphinx.%s\0",m_sName.cstr());
    }
    char* idstr = (char*)malloc(100);
#endif // USE_MONGOC

...

        for ( int i=0; i<pRes->m_iCount; i++ )

...

//mongodb 에서 id로 레코드 조회, (레코드 추출 for 문 뒤)
#if USE_MONGOC
            bool mongoFound = false;
            sprintf(idstr,"%ld",tMatch.m_iDocID);
            std::auto_ptr<mongo::DBClientCursor> mongoCursor = mongoConn.query(collectionName, BSON("_id"<<idstr));
            BSONObj mongoDoc;
            if(mongoCursor->more()) {
                mongoDoc = mongoCursor->next();
                mongoFound = true;
            }
#endif

...

            ARRAY_FOREACH ( j, dStringItems )

...

//추출한 레코드 내 문자열 데이터에 관해서.
#ifndef USE_MONGOC
//mongodb 를 사용하지 않는경우 원래의 프로세스
                    iRespLen += rLen;
#else
//mongodb 를 사용하는 경우 텍스트 데이터를 조회하여 길이를 측정한다.
                    int colInx = dStringItemIndex[j];
                    if(mongoFound) {
                        const char* data = mongoDoc.getStringField(pRes->m_tSchema.GetAttr(colInx).m_sName.cstr());//mongoc->MongoGetString(tAttr.m_sName.cstr());
                        if(data) {
                            iRespLen += strlen(data);
                        } else {
                            iRespLen += rLen;
                        }
                    } else {
                        iRespLen += rLen;
                    }
#endif

...

//모두 종료 후 할당된 메모리 해제
#if USE_MONGOC
        free(idstr);
#endif //USE_MONGOC

...

//----------------------------------------검색결과데이터전송부
void SendResult (CSphString m_sName, int iVer, NetOutputBuffer_c & tOut, const CSphQueryResult * pRes, 
    const CSphTaggedVector & dTag2Pools, bool bAgentMode, const CSphQuery & tQuery, int iMasterVer, std::map<int, void*>m_mContext )
{
//mongo driver 초기화
#if USE_MONGOC
    mongo::DBClientConnection mongoConn;
    mongoConn.connect(g_sMongoAddr.cstr());
    char collectionName[strlen(m_sName.cstr()) + 7]; 
    {
        sprintf(collectionName,"sphinx.%s\0",m_sName.cstr());
    }
    char* idstr = (char*)malloc(100);
#endif // USE_MONGOC

...

    for ( int i=0; i<pRes->m_iCount; i++ )

...

//mongodb 에서 id로 레코드 조회, (레코드 추출 for 문 뒤)
#if USE_MONGOC
            bool mongoFound = false;
            sprintf(idstr,"%ld",tMatch.m_iDocID);
            std::auto_ptr<mongo::DBClientCursor> mongoCursor = mongoConn.query(collectionName, BSON("_id"<<idstr));
            BSONObj mongoDoc;
            if(mongoCursor->more()) {
                mongoDoc = mongoCursor->next();
                mongoFound = true;
            }
#endif

...

            for ( int j=0; j<iAttrsCount; j++ )

...

//추출한 레코드 내 칼럼 조회
#ifndef USE_MONGOC
//mongodb 를 사용하지 않는 경우 원래의 프로세스
                            tOut.SendDword ( iLen );
                            tOut.SendBytes ( pStr, iLen );
#else
//mongodb 를 사용하는 경우 텍스트데이터를 조회하여 네트워크 스트림으로 출력한다.
                            int tLen = 0;
                            if(mongoFound) {
                                const char* data = mongoDoc.getStringField(tAttr.m_sName.cstr());
                                if(data!=NULL) {
                                    tLen = strlen(data);
                                    pStr = (BYTE*)data;
                                } else {
                                    tLen = iLen;
                                }
                            }
                            tOut.SendDword ( tLen );
                            tOut.SendBytes ( pStr, tLen );
#endif //USE_MONGOC

...

//모두 종료 후 할당된 메모리 해제
#if USE_MONGOC
    free(idstr);
#endif //USE_MONGOC


//----------------------------------------SphinxQL 용 검색결과데이터전송부
void SendMysqlSelectResult ( CSphString m_sName, SqlRowBuffer_c & dRows, const AggrResult_t & tRes,
    bool bMoreResultsFollow, std::map<int, void*> m_mContext)

...

//mongo driver 초기화
#if USE_MONGOC
    mongo::DBClientConnection mongoConn;
    mongoConn.connect(g_sMongoAddr.cstr());
    char collectionName[strlen(m_sName.cstr()) + 7];
    {   
        sprintf(collectionName,"sphinx.%s\0",m_sName.cstr());
    }
    char* idstr = (char*)malloc(100);
#endif // USE_MONGOC

...

    for ( int iMatch = tRes.m_iOffset; iMatch < tRes.m_iOffset + tRes.m_iCount; iMatch++ )

...

//mongodb 에서 id로 레코드 조회
#ifdef USE_MONGOC
        bool mongoFound = false;
        sprintf(idstr,"%ld",tMatch.m_iDocID);
        std::auto_ptr<mongo::DBClientCursor> mongoCursor = mongoConn.query(collectionName, BSON("_id"<<idstr));
        BSONObj mongoDoc;
        if(mongoCursor->more()) {
            mongoDoc = mongoCursor->next();
            mongoFound = true;
        }
#endif

...

        for ( int i=0; i<iSchemaAttrsCount; i++ )

...

//칼럼조회
#ifndef USE_MONGOC
//mongodb 를 사용하지 않는 경우 원래의 프로세스
                    uOffset = (DWORD) tMatch.GetAttr ( tLoc );
                    if ( uOffset )
                    {
                        assert ( pStrings );
                        iLen = sphUnpackStr ( pStrings+uOffset, &pStr );
                    }
#else
//mongodb 를 사용하는 경우 텍스트데이터를 조회하여 버퍼에 출력.
                    sphUnpackStr ( pStrings+uOffset, &pStr );
                    if(mongoFound) {
                        const char* data = mongoDoc.getStringField(tSchema.GetAttr(i).m_sName.cstr());
                        if(data) {
                            iLen = strlen(data);
                            pStr = (BYTE*)data;
                        }
                    }
#endif //USE_MONGOC

...

//모두 종료 후 할당된 메모리 해제
#if USE_MONGOC
    free(idstr);
#endif //USE_MONGOC
indexer.cpp / 추가분
...

    SqlAttrsConfigure ( tParams,    hSource("sql_field_string"),        SPH_ATTR_STRING, sSourceName, true, true );
     // sql_mongo_string 타입 생성, 색인은 생성하되 sphinx 도큐먼트로 보내지는 않는다.
     SqlAttrsConfigure ( tParams,    hSource("sql_mongo_string"),        SPH_ATTR_STRING, sSourceName, true );
    SqlAttrsConfigure ( tParams,    hSource("sql_field_str2wordcount"), SPH_ATTR_WORDCOUNT, sSourceName, true );
...
sphinxutils.cpp / 추가분
...

    { "sql_field_string",       KEY_LIST, NULL },
    //sql_mongo_string 타입 스키마 정의 
    { "sql_mongo_string",       KEY_LIST, NULL },
    { "sql_field_str2wordcount",    KEY_LIST | KEY_DEPRECATED, "index_field_lengths" },

...

static KeyDesc_t g_dKeysCommon[] =
{
    // mongo_addr 속성 정의
    { "mongo_addr",             0, "localhost:27017" },
    { "lemmatizer_base",        0, NULL },

...


void sphConfigureCommon ( const CSphConfig & hConf )
{
    if ( hConf("common") && hConf["common"]("common") )
    {   
        CSphConfigSection & hCommon = hConf["common"]["common"];
        g_sLemmatizerBase = hCommon.GetStr ( "lemmatizer_base" );

        //mongo_addr 속성 읽어오기 (전역변수로 포함)
        g_sMongoAddr = hCommon.GetStr ( "mongo_addr" );

...


소스 변경 기타 사항

Searchd 의 호출순서 :

1. sphinx 클라이언트 의 경우
  • main → ServiceMain → TickHead → HandlClient → HandleClientSphinx → HandleCommandSearch → SendSearchResponse → SendResult
2. sphinxql (Mysql 클론) 의 경우
  • main → ServiceMain → TickHead → HandlClient → HandleClientMysql → CsphinxqlSession::Execute → SendMysqlSelectResult


함수 비고
ServiceMain 설정파일을 파싱하고 TickHead 를 사용하여 데몬 실행
TickHead 소켓을 열고 요청이 있으면 HandleClient 에 넘김
HandleClient 요청타입에 따라 SphinxClient 와 SphinxQL 핸들러로 넘김
HandleClientSphinx 요청된 패킷을 분해조립 하여 Command로 나누고 수행
HandleCommandSearch 요청을 Query화 하여 검색 수행
SendSearchResponse 네트워크 스트림에 결과 태워 보냄 (다중질의)
SendResult 네트워크 스트림에 검색결과를 태워보냄 (단일질의)
CsphinxqlSession::Execute Sphinxql SQL 문을 파싱하여 검색 수행
SendMysqlSelectResult 검색된 결과를 네트워크 스트림에 태워 보냄


기타

검색엔진의 document-backend 를 자체저장에서 mongo-db 로 바꾸었으나, 일부 기능 (정렬기능, 필터기능) 은 자체저장 방식에서만 가능하므로 따로 저장타입으로 sql_mongo_string 라는것을 두어 정렬, 필터 를 지원할 필요가 없는 장문 데이터 는 mongodb 에서만 저장하도록 하였음,


숫자형, 날자형 타입의 데이터는 반드시 정렬과 필터기능을 사용하므로 mongodb 백엔드를 사용할 수 없음.