원본 본문으로 이동하기

검색엔진 솔라 시작하기

박용서 - 얼마 전 검색엔진 도입에 대한 질문이 올라와서 이 강의를 쓰게 되었습니다. 검색엔진이란? 검색엔진은 흔히 알고있는 관계형 데이터베이스 (RDBMS) 처럼 명확한 정보를 찾는 것이 아닌 데이터를 일종의 "단어"단위로 쪼개서 저장하게 되며, 해당 단어를 중점적으로 찾는 데이터베이스 엔진 입니다. 때문에 RDBMS 처럼 순차적인 검색이 아닌 비 순차적 검색에서 빠른 속도를 확보할 수 있습니다. 검색엔진 도입의 필요성 필자의 경우는 50여 가지의 사이트를 만들면서, 딱 두번 유저가 많은 사이트를 만든 적이 있었습니다. (하지만.. 가리사니는........) 게시물이 점점 많아져서 몇 만 개의 게시물이 쌓이기 시작하면 기존 RDBMS 의 LIKE 검색으로는 감당이 안될 정도의 데이터가 쌓이게 되고, 대부분의 커뮤니티들은 이를 [다음검색] 으로 끊어 검색하는 방법으로 해결해 왔습니다. (감당이 되더라도.. 인덱스를 탈 수 없는 LIKE 는 지양하시기 바랍니다.) 이런 상황에서 검색엔진을 도입 할 경우, 사용자가 [다음검색] 으로 끊어 검색하는 불편함도 사라지게 되며, 자연어 검색이나 스코어별 정렬, 다양한 조건 (예: 이름이 홍길동이며 내용에 ~~ ~~ 단어가 들어가는 것) 에서 더욱 강점을 보입니다. 검색엔진 종류 - http://db-engines.com/en/ranking/search+engine - 루씬은 라이브러리로 분류되어 빠져있습니다. 국내에선 아래 4개를 많이 사용합니다. 루씬 Elasticsearch (루씬기반) Solr (루씬기반) Sphinx 여기서는 솔라를 설치해보도록 하겠습니다. 1. 솔라 설치 참고 : https://gs.saro.me/#!m=pd&pn=6 - 솔라를 다운받습니다. (tgz, zip 상관없음 압축만 풀면됩니다.) - 압축을 풀어봅니다. 아래와 같은 구조가 보일겁니다. 솔라루트 ├ bin ├ contrib ├ dist ├ docs ├ example ├ licenses ├ server ├ CHANGES.txt ├ LICENSE.txt ├ LUCENE_CHANGES.txt ├ NOTICE.txt └ README.txt - example/example-DIH/solr의 db 디렉토리를 server/solr/ 로이동시킵니다. server/solr/db - db 디렉토리명을 first로 변경합니다. server/solr/first - 이렇게 first 프로젝트가 생성되었습니다. 이름은 first 가 아닌 다른걸로 해도 무방하나 여기선 first 를 기준으로 설명합니다. 이름규칙은 변수규칙과 비슷합니다. 여기에선 예제 파일을 활용하는 쪽으로 진행하도록 하겠습니다. - 솔라루트에서 server, bin, contrib, dist 를 제외하고 다 지웁니다. - _LIB 폴더를 만들어줍니다. : 외부 jar 포함 폴더 (이름은 자유지만 여기선 _LIB 로 설명됩니다.) 아래와 같은 구조가 되면됩니다. 솔라루트 ├ _LIB : 여기에 각종 JDBC나 한글형태소등을 넣어줍니다. │ ( 한글 형태소 참고 : http://cafe.naver.com/korlucene ) ├ bin ├ contrib ├ dist └ server - _LIB 폴더안에 JDBC 를 넣어주세요. (여기선 JDBC 를 짧게만 설명하니 아무것도 안하셔도됩니다.) 하지만 아직 설정해야 할 것이 많음으로 실행시키시면 안됩니다. 2. first 프로젝트 설정 - server/first/conf 로 이동합니다. - solrconfig.xml 을 열고 아래와 같이 작업합니다. <lib dir="${solr.install.dir:../../../..}/contrib/extraction/lib" regex=".*\.jar" /> <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-cell-\d.*\.jar" /> <lib dir="${solr.install.dir:../../../..}/contrib/langid/lib/" regex=".*\.jar" /> <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-langid-\d.*\.jar" /> <lib dir="${solr.install.dir:../../../..}/contrib/velocity/lib" regex=".*\.jar" /> <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-velocity-\d.*\.jar" /> - 위와 같이 써있는 부분을 찾아서 아래와 같이 추가해줍니다. <lib dir="${solr.install.dir:../../../..}/_LIB/" regex=".*\.jar" /> - managed-schema 파일을 오픈합니다. (버전에 따라서는 schema.xml 이나 현재 글쓰는 시점의 최신버전 6.0.0 에서는 managed-schema 입니다.) =========================================== 여기서 알아둬야할 것! - field 은 데이터베이스의 열과 같음! - dynamicField / fieldType 은 자료형 (엄밀히 말하면 자료형은 아니고 필드타입이지만.... [설정기능들이 많음] 이해를 돕기위해) 입니다. - _version_ field 는 필수입니다. - 1개의 유니크 필드가 필요하고 <uniqueKey>필드이름</uniqueKey> 으로 저장합니다. - 기본 검색필드의 예제에서 값은 text 입니다. 이걸 수정할 경우 schema.xml 에서 <str name="df">text</str> 된 모든 값을 바꿔야합니다. - fieldType 을 보시면 각종 필드들이 선언되어있고 여기서 자신이 수정하여 사용할수 있으며 아래 예제설명 <!-- 필드타입의 이름은 text_en 이다. --> <fieldType name="text_en" class="solr.TextField" positionIncrementGap="100"> <!-- 필드에 인덱싱 (type="index")될 때 거치는 분석규칙 --> <analyzer type="index"> <tokenizer class="solr.StandardTokenizerFactory"/> <filter class="solr.SynonymFilterFactory" synonyms="index_synonyms.txt" ignoreCase="true" expand="false"/> <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_en.txt" /> <filter class="solr.LowerCaseFilterFactory"/> <filter class="solr.EnglishPossessiveFilterFactory"/> <filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt"/> <filter class="solr.PorterStemFilterFactory"/> </analyzer> <!-- 필드에 검색할때 검색어(type="query")가 거치는 분석규칙 --> <analyzer type="query"> <tokenizer class="solr.StandardTokenizerFactory"/> <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/> <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_en.txt"/> <filter class="solr.LowerCaseFilterFactory"/> <filter class="solr.EnglishPossessiveFilterFactory"/> <filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt"/> <filter class="solr.PorterStemFilterFactory"/> </analyzer> </fieldType> - 분석규칙이라는 말을 썼는데 예를들어 한글형태소(위 루씬 네이버 카페 http://cafe.naver.com/korlucene 에 가면 구할 수 있습니다.)라고 한다면 아래와 같이 저장/검색됩니다. (예제에서 보여주는 한글을 쪼개는 행위는 예제입니다. 실제 규칙이 너무 많아서 직접 돌려봐야 알 수 있습니다. 아래처럼 깔끔하게 잘리진 않습니다...;;) 인덱싱 예제 : 데이터 -> 가리사니 개발자공간에 사람들이 많이 왔으면 좋겠어요. -> 분석필터 -> 가리사니 / 개발자 / 공간 / 사람 / 많이 / 왔으면 / 좋겠 검색(쿼리) 예제 : 데이터 -> 가리사니에 사람들이 많이 있나요? -> 분석필터 -> 가리사니 / 사람 / 사람들 / 많이 즉, 이런식으로 해서 위 인덱싱 예제는 쿼리 예제에 대해 검색되게 됩니다. - 위에서도 말했지만.. 아직 솔라는 한글 형태소를 지원하지 않기 때문에 직접 만들거나 위 카페에서 받은 방법등이 있습니다. - 지원 해 준다고 해도 전문적으로 쓰려면 직접 필터를 코딩해서 추가 해줍니다. - 가라사니의 태그예제는 직접 만들진 않고 섞어 사용하는데, 아래와 같이 되어있습니다. <fieldType name="gs_tag" class="solr.TextField" positionIncrementGap="100"> <analyzer type="index"> <!-- 저장시에는 대소문자 구분을 없에고(LowerCaseFilterFactory) 공백만 보고 잘라서 저장한다는 뜻입니다. --> <filter class="solr.LowerCaseFilterFactory" /> <tokenizer class="solr.WhitespaceTokenizerFactory" /> </analyzer> <analyzer type="query"> <!-- 검색할때는 아래와 같이 한글을 모두분리하여 사용합니다. --> <filter class="solr.LowerCaseFilterFactory" /> <tokenizer class="org.apache.lucene.analysis.ko.KoreanTokenizerFactory" /> <filter class="solr.LengthFilterFactory" min="1" max="50" /> <filter class="org.apache.lucene.analysis.ko.KoreanFilterFactory" hasOrigin="true" hasCNoun="true" /> <filter class="org.apache.lucene.analysis.ko.WordSegmentFilterFactory" hasOrijin="true" /> <filter class="org.apache.lucene.analysis.ko.HanjaMappingFilterFactory" /> <filter class="org.apache.lucene.analysis.ko.PunctuationDelimitFilterFactory" /> <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" /> </analyzer> </fieldType> - 주제가 너무 멀리 떨어지니 계속 진행하도록 하겠습니다... =========================================== - 모든 <copyField>와 <field> 노드를 삭제합니다. - 아래와 같이 추가해줍니다. <!-- 반드시 필요 --> <field name="_version_" type="long" indexed="true" stored="true"/> <!-- 유니크값 --> <field name="id" type="tlong" indexed="true" stored="true"/> <!-- 제목 --> <field name="subject" type="text_en" indexed="true" stored="false"/> <!-- 내용 --> <field name="text" type="text_en" indexed="true" stored="false"/> - type 은 위에서 설명한 fieldType 의 이름입니다. - indexed : 인덱스 여부, stored : 원문 저장여부 - 이렇게 해서 하나의 유니크 아이디를 가지고 있고 제목 / 내용이 있는 스키마를 완성했습니다. - 저장 (사실 예제에 의존하지 않고 차근차근 필요한 것만 작성하는 것이 좋지만... 그걸 설명하려면 이 강의가 너무 길어질 것 같아 이렇게 나갑니다.) - db-data-config.xml 열기 - JDBC 를 jar를 _LIB에 넣었고 위 id / subject / text 에 대응하는 테이블이 있다면 직접 적어 주시면됩니다. - 다만 이 부분을 스킵하려면 아래와 같이 주석처리합니다. <dataConfig> <!-- <dataSource driver="org.hsqldb.jdbcDriver" url="jdbc:hsqldb:${solr.install.dir}/example/example-DIH/hsqldb/ex" user="sa" /> <document> <entity name="item" query="select * from item" deltaQuery="select id from item where last_modified > '${dataimporter.last_index_time}'"> <field column="NAME" name="name" /> <entity name="feature" query="select DESCRIPTION from FEATURE where ITEM_ID='${item.ID}'" deltaQuery="select ITEM_ID from FEATURE where last_modified > '${dataimporter.last_index_time}'" parentDeltaQuery="select ID from item where ID=${feature.ITEM_ID}"> <field name="features" column="DESCRIPTION" /> </entity> <entity name="item_category" query="select CATEGORY_ID from item_category where ITEM_ID='${item.ID}'" deltaQuery="select ITEM_ID, CATEGORY_ID from item_category where last_modified > '${dataimporter.last_index_time}'" parentDeltaQuery="select ID from item where ID=${item_category.ITEM_ID}"> <entity name="category" query="select DESCRIPTION from category where ID = '${item_category.CATEGORY_ID}'" deltaQuery="select ID from category where last_modified > '${dataimporter.last_index_time}'" parentDeltaQuery="select ITEM_ID, CATEGORY_ID from item_category where CATEGORY_ID=${category.ID}"> <field column="DESCRIPTION" name="cat" /> </entity> </entity> </entity> </document> --> </dataConfig> - elevate.xml 도 열어서 아래와 같이 바꿔줍니다. <?xml version="1.0" encoding="UTF-8" ?> <elevate></elevate> 3. first 프로젝트 실행 - bin 폴더로 이동합니다. > solr start -p 8888 - 8888 은 포트번호 입니다. 여기서는 8888로 설명되나 원하는 포트를 설정해주세요. - 옵션은 종류가 정말 다양하지만 여기서는 다루지않습니다. - 직접 solr 배치를 열어보시면 모든 옵션을 보실 수 있습니다. - 접속 : http://127.0.0.1:8888/ - 깔끔한 관리도구가 나옵니다.!! - Core Selector 에서 first 를 선택합니다. - 아까 JDBC를 추가하고 db-data-config.xml 를 id / subject / text 에 대응하여 작성하신분들은 dataimport 에서 import 해줍니다. - 그럼 이제 예제로 추가해보겠습니다. ====================================================== 쿼리 쓰는법 - 여기서는 url 방식으로 해보겠습니다. (여러가지 방법이 있습니다.) - 자바에서 날리거나 할 때에는 용량상 get 이 아닌 post 로 날리시기 바랍니다. 추가/수정 http://127.0.0.1:8888/solr/first/update?commit=true&stream.body={add:[{id:1, subject:"안녕하세요",text:"예제 입니다."}]} 삭제 : id의 숫자는 해당 문서의 유니크키 http://127.0.0.1:8888/solr/first/update?commit=true&stream.body={delete:[{id:1},{id:2}]} 당연히 안녕하세요 이런 것들 전부 인코딩 하셔서 넣으셔야 합니다. 윈도우 크롬 기준으로 그냥 저렇게 넣어도 들어가진 하지만... 예제이니... 실제로는 전부 인코딩 해서 넣으시기 바랍니다. ====================================================== - 위 방법대로 하나 추가를 해봅니다. - http://127.0.0.1:8888/solr/#/first/query 가서 검색을 해봅니다. - 처음엔 *:* 식으로 되어있지만 예를들어 text:안녕 이라고 바꾸고 서브밋을 하면 아래와 같이 나올겁니다. { "responseHeader":{ "status":0, "QTime":0, "params":{ "q":"text:예제", "indent":"on", "wt":"json"}}, "response":{"numFound":1,"start":0,"docs":[ { "id":1, "_version_":1532604497861279744}] }} - 검색시 상단에 있는 주소 예를들어 아래와 같이 치면. http://127.0.0.1:8888/solr/first/select?indent=on&q=text:%EC%98%88%EC%A0%9C&wt=json 이제 응용해서 만드시기만 하면 됩니다.!! - 솔라