'lucene'에 해당되는 글 4건
- 2015.11.23 :: Lucene 활용 - search
- 2015.11.23 :: lucene 기초 -search
- 2015.10.30 :: lucene 기초 - index
- 2015.10.30 :: lucene 기초
개인적으로 Lucene을 사용하다보면 검색 쿼리를 객체로 컨트롤 하기가 귀찮아 진다. 여기서 객체란 BooleanQuery, TermQuery, FuzzyQuery, WildCardQuery등등 있다. 이런것들을 이용하여 쿼리를 만들 수 있으나 String 형태로 쿼리를 만들 수도 있다. 이번엔 그런 것들을 해보자.
먼저 Lucene도 3가지 옵션을 제공한다. Must, Should, MustNot 이 옵션 세가지를 제공한다.
1. Must - And와 같다.
2. Should - or와 같다.
3. Must Not - Not And와 같다.
그리고, 쿼리는 다음과 같은 형태로 변환이 된다.
(+text:cross) ((html:good) (+html:boat)) 이런식으로 쿼리가 변환이 된다. 이걸 다시 풀어보면..
text필드의 cross를 And 검색하라.
html필드를 good을 or로 검색하고 html에 boat는 and로 검색하라. 그리고 이전체를 or로 검색하라 . 라는 결론이 된다.
물론 단일 term의 경우 and나 or나 결과는 같다. 하지만 ((html:good) (+html:boat)) 이 구문의 경우 (+html:good +html:boat) 이런 형태로도 나올 것이다. 일종의 sql의 where과 흡사하다.
위에 예제와 같이 여러개의 쿼리를 필요 할때는 BooleanQuery를 루씬에서 제공한다. 그럼 만들어보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | //생략////////////////////////////////////////// public String tempAndAnalyze(String string[], String field, Analyzer analyzer) throws IOException { StringBuffer buffer = new StringBuffer(); buffer.append("("); for(String str : string){ buffer.append("+"); // buffer.append(field); // buffer.append(":"); buffer.append(str).append(" "); } buffer.append(")"); return buffer.toString(); } @Test public void testHtmlBasicSearchData2() { String andWordSearch = "cross sectional"; if (andWordSearch != null && andWordSearch != "") { try { // searchIndex(andWordSearch, textField); String queryStr = tempAndAnalyze(andWordSearch.split(" "), "text"); Query andQuery = new QueryParser(Version.LUCENE_36, "text", new StandardAnalyzer(Version.LUCENE_36)).parse(queryStr); // #B TopDocs hits = searcher.search(andQuery, searcher.maxDoc()); dumpHits(searcher, hits, "text"); } catch (ParseException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } | cs |
위에 설명한 대로 cross sectional 쿼리를 String으로 만들고 그것을 다시 stringbuffer에 저장해서 QueryParser로 전달했다. 갠적으론 이방법을 많이 쓴다.
여기서 tip 한개더. 루씬은 stanardAnalyzer를 space+stop word(내부에 저장) 를 기반으로 단어를 나눈다. 이때 중요한 점은 lowcase는 적용을 안한다는 사실이다. 이 때문에 standard를 쓸 시 필히 대소문자를 구분해야한다.
or문의 경우 tempAnalyze의 경우 +를 삭제 해주면 된다. 다음에는 다중 검색에 대해 알아보자.
루씬은 색인과 검색 두가지로 나뉜다. 기존 색인을 검색 할 때도 쓰이지만, 특정 문장을 검증 할때도 쓰인다.
루씬의 version은 3.6 기준으로 작성하였다.
먼저 검색은 두가지 객체가 중요하다. IndexReader와 IndexSearcher 여기서 reader는 특정 색인의 접근을 해주는 객체이며, searcher는 그 색인을 찾고자 하는 검색 룰을 전달해주는 넘이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | private IndexSearcher searcher; private Directory dir; private IndexReader reader; private String textField; @Before public void setup() throws IOException { textField = "text"; dir = FSDirectory.open(new File(dirPath)); reader = IndexReader.open(dir); searcher = new IndexSearcher(reader); } @After public void tearDown() throws IOException { searcher.close(); reader.close(); } | cs |
먼저 초기 셋팅을 해준다. 기존에 만들어 놓은 index경로는 dirPath가 될 것이다. 검색 필드는 "text"로 지정한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | private ArrayList<Document> dumpHits(IndexSearcher searcher, TopDocs hits, String fieldName) throws IOException { ArrayList<Document> docList = null; if (hits.totalHits == 0) { return null; } docList = new ArrayList<Document>(); for (ScoreDoc match : hits.scoreDocs) { Document doc = searcher.doc(match.doc); } return docList; } public void searchIndex(String searchString, String field) throws ParseException, IOException { System.out.println("\nSearching for '" + searchString + "' using QueryParser"); QueryParser queryParser = new QueryParser(Version.LUCENE_36, field, new StandardAnalyzer(Version.LUCENE_36)); Query query = queryParser.parse(searchString); System.out.println("Type of query: " + query.getClass().getSimpleName()); TopDocs hits = searcher.search(query, searcher.maxDoc()); dumpHits(searcher, hits, field); } @Test public void testHtmlBasicSearchData() { String andWordSearch = "cross"; if (andWordSearch != null && andWordSearch != "") { try { searchIndex(andWordSearch, textField); } catch (ParseException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } | cs |
가장 기본인 검색이다... (개인적으로 이방법은 안쓴다.. =_=; 찾는것도 힘드네 ㅋㅋ)
여기서 중요한 함수는 searchIndex라는 함수다.
중요한 객체는 총 3가지 , QueryParser와 Query, TopDocs 이 객체만 알면 Lucene을 가지고 분석을 자유롭게 할 수 있을 것이다. 먼저 한개씩 설명하자면,
1. QueryParser : 입력한 Query문을 Lucene이 알아먹을 수 있는 Query로 변환한다. 쉽게 말하자면 당신이 넣은 쿼리를 선택한 분석기에 맞게 쿼리 객체 형태로 반환한다.
2. Query : QueryParser로 변환된 쿼리 객체를 searcher 객체로 전달한다. (Query의 종류는 상당히 많으며, 최상위 객체가 Query이다. 종류는 TermQuery, BooleanQuery, Fuzzy, WildCard 등등 있다)
3. TocDocs : 검색된 결과를 보여준다.
위의 결과를 돌려보면
1 2 3 4 5 | Searching for 'cross' using QueryParser Type of query: TermQuery | cs |
이런 형태가 나오고 dubugging모드로 Query를 보면 text:cross 란 쿼리로 변환이 될것이다.
아마도 default Query는 TermQuery 이며, 객체를 통해 Lucene이 알아먹을 수 있는 형태의 쿼리로 변환 되는 듯 보인다.
이후 좀더 복잡한 쿼리에 대해 알아보겠다.
이번 글에선 색인에 대해 알아보자.
색인은 I am a boy. 라는 단어가 있으면 특정 분석기(analyzer)를 통해 분석이 되며 이 분석된 단어(Term)들이 색인 되어 진다. lucene은 rdb와 틀리게 증분색인으로 이루어져 있다.
WhiteSpaceAnalyzer 를 사용하면 위 문장은 I, Am, A, Boy로 분석이 된다.
StandardAnalyzer는 Boy 정도만 나올 것이다. 이유는 내부적으로 Stopword를 가지고있다.
결국 StandardAnalyzer는 WhiteSpaceTokenizer+StopFilter+lowercaseTokenizer 정도로 구성된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | @ContextConfiguration(locations={ "file:src/main/webapp/WEB-INF/spring/root-context.xml"}) @RunWith(SpringJUnit4ClassRunner.class) public class TestHtmlIndexer { private static final Logger logger = LoggerFactory.getLogger(TestHtmlIndexer.class); private Directory dir = null; // @Autowired // private WhitespaceAnalyzer whitespaceAnalyer; @Autowired private StandardAnalyzer standardAynalyzer; private CustomSimpleAnalyzer customAnalyzer; private WhitespaceAnalyzer whitespaceAnalyzer; private SimpleAnalyzer simpleAnalyzer; private IndexWriter writer; @Autowired private HtmlWithTikaParser htmlParser; @Autowired private TieredMergePolicy tmp; @Value("${fileindex}") private String path; @Before public void setup() throws IOException, InterruptedException{ customAnalyzer = new CustomSimpleAnalyzer(Version.LUCENE_36); //저장 방식에는 많이쓰는 방식이 몇 가지를 지원하는데, NIODirctory, SimpleDirectory,FSDirectory, RAMDictory등 //RAMDirtory - 메모리에 index를 저장 테스트시 많이 사용. //NIODirectory - unix계열에서만 가능 하지만 버그가 있는것으로 알고있음. //FSDirectory - 이걸 가장 많이 쓴다고 함. //디렉토리를 open dir = FSDirectory.open(new File(path)); //어떤 색인으로 할것인지 대한 설정. IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_36, customAnalyzer); //색인은 어떤 형태로 저장할지 에 대한 셋팅. //OpenMode.CREATE - 색인시마다 기존 색인 삭제 후 재 색인 //OpenMode.CREATE_OR_APPEND - 기존 색인이 없으면 만들고, 있으면 append 함. //OpenMode.APPEND - 기존 색인에 추가. iwc.setOpenMode(OpenMode.CREATE_OR_APPEND); //색인 파일의 병합 전략인데 사실 읽어봐도 이해를 못해서 걍 씀. iwc.setMergePolicy(tmp); //lucene은 rdb와 다르게 lock에 대한 처리를 안해준다. 단지, index시 lock 파일이 존재하면 error가 발생한다. //이 때문에 필히 lock 체크를 해줘야함. lockChecker(); //드디어 indexwrite 생성. 디렉토리와 config를 매개변수로.... writer = new IndexWriter(dir, iwc); } public void lockChecker() throws IOException, InterruptedException { //IndexWriter.WRITE_LOCK_NAME - 실제 index 시 directory에 보면 xxx.lock 파일이 존재하게 되는데 //존재 할 경우는 lock으로 판단. while(dir.fileExists(IndexWriter.WRITE_LOCK_NAME)){ // dir.clearLock(name); Thread.sleep(10); } } public void addDocument(HtmlDTO dto){ try { writer.addDocument(dto.convetDocument()); } catch (CorruptIndexException e) { // TODO Auto-generated catch block logger.error(e.getMessage()); } catch (IOException e) { // TODO Auto-generated catch block logger.error(e.getMessage()); } } public void writeClose() throws CorruptIndexException, IOException{ if(writer != null){ writer.close(); } } @Test public void testAddDocument() throws CorruptIndexException, IOException, SAXException, TikaException{ URL url = this.getClass().getClassLoader().getResource("html/xxx.json"); String path = url.getPath(); File file = new File(path); JSONParser parser = new JSONParser(); try { Object obj = parser.parse(new FileReader(path)); JSONObject jsonObject = (JSONObject) obj; Iterator<String> keys = jsonObject.keySet().iterator(); while(keys.hasNext()){ String key = keys.next(); JSONObject valueObj = (JSONObject) jsonObject.get(key); String filepath = valueObj.get("FilePath").toString(); String CATEGORY_TEXT_ID = valueObj.get("CATEGORY_TEXT_ID").toString(); String breadcrumb = valueObj.get("Breadcrumb").toString(); String CATEGORY_TREE = valueObj.get("CATEGORY_TREE").toString(); String CATEGORY_ID = valueObj.get("CATEGORY_ID").toString(); String LOCALE_KEY = valueObj.get("LOCALE_KEY").toString(); String CATEGORY_TITLE = valueObj.get("CATEGORY_TITLE").toString(); String CATEGORY_DESC = valueObj.get("CATEGORY_DESC").toString(); HtmlDTO dto = new HtmlDTO(); dto.setCategoryTextId(Integer.parseInt(CATEGORY_TEXT_ID)); dto.setCategoryTree(CATEGORY_TREE); dto.setBreadcrumb(breadcrumb); dto.setCategoryId(Integer.parseInt(CATEGORY_ID)); dto.setLocaleKey(LOCALE_KEY); dto.setCategoryTitle(CATEGORY_TITLE); dto.setCategoryDesc(CATEGORY_DESC); url = this.getClass().getClassLoader().getResource("html/"+filepath); // 이부분 수정. ArrayList<String> list = htmlParser.htmlParser(url.getPath()); dto.setText(list.get(0)); dto.setHtml(list.get(1)); addDocument(dto); } } catch (FileNotFoundException e) { logger.error(e.getMessage()); } catch (IOException e) { logger.error(e.getMessage()); } catch (ParseException e) { logger.error(e.getMessage()); } } @After public void tearDown() throws CorruptIndexException, IOException{ writeClose(); } } | cs |
위 예제는 json을 읽어서 특정 경로에 있는 html을 색인하는 과정을 junit으로 해본것이다.
convertDocument() 는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | ..생략........ public Document convetDocument() { // TODO Auto-generated method stub Document doc = new Document(); NumericField categoryTextIndex = new NumericField("categoryTextId",Field.Store.YES,true); categoryTextIndex.setIntValue(this.getCategoryTextId()); doc.add(categoryTextIndex); // NumericField categoryId = new NumericField("categoryId",Field.Store.YES,true); categoryId.setIntValue(this.getCategoryId()); doc.add(categoryId); doc.add(new Field("categoryTree",this.getCategoryTree(),Field.Store.YES,Field.Index.NOT_ANALYZED )); doc.add(new Field("localeKey",this.getLocaleKey(),Field.Store.YES,Field.Index.NOT_ANALYZED )); doc.add(new Field("breadcrumb",this.getBreadcrumb(),Field.Store.YES,Field.Index.NOT_ANALYZED )); doc.add(new Field("categoryTitle",this.getCategoryTitle(),Field.Store.YES,Field.Index.NOT_ANALYZED )); doc.add(new Field("categoryDesc",this.getCategoryDesc(),Field.Store.YES,Field.Index.NOT_ANALYZED )); doc.add(new Field("text", this.getText(), Field.Store.YES, Field.Index.ANALYZED, TermVector.WITH_POSITIONS_OFFSETS)); doc.add(new Field("html", this.getHtml(), Field.Store.YES, Field.Index.ANALYZED, TermVector.WITH_POSITIONS_OFFSETS)); return doc; } ......생략............... | cs |
Field의 매개변수는 org.apache.lucene.document.Field.Field(String name, String value, Store store, Index index, TermVector termVector) 또는 org.apache.lucene.document.Field.Field(String name, String value, Store store, Index index) 를 많이 쓴다.
store의 옵션은 총 2가지 이며, 'Field.Store.YES 는 value를 저장 할 것인다.' 이며
'Field.Store.NO 는 value를 저장 안 할 것인다.' 이다.
이말은 value는 단순히 Field의 plain text를 말하는 것이지 index된 값을 말하는것이 아니다.
Field.Index의 옵션은 총 3가지이며 NOT_ANALYZED,NO, ANALYZED 가 있다.
NOT_ANALYZED는 field의 값을 분석을 안한다는 말이며, plaintext의 값과 검색 시 비교는 할 수있다. rdb의 특정 컬럼 비교라고 생각 하면 된다.
NO의 경우는 검색을 지원하지 않는다. 단순한 값 저장 시 사용된다.
ANALYZED는 Field의 값을 분석을 하며 이 분석된 값을 색인으로 만든다.
TermVector의 경우, 색인의 특정 값을 보고자할때(?) 사실 본인의 경우 debugging용으로 사용 또는 term 추출 시 사용한다.
TemrVector는 index의 offset, 등장 횟수 등을 저장한다.
다음 글 부터 검색 과정을 예제 소스에 대해 자세히 설명하겠다.
루씬은 document기반의 저장방식으로 각 document의 field를 색인 하게 된다.
색인은 각 분석기의 특징대로 색인을 하며, 검색 시 그 색인을 기반으로 검색하게 된다.
db와 틀린점은 머 내생각이니 다음과 같은데...
1. db는 특정 컬럼에 값을 insert 한다는 개념이 강하다. 루씬은 insert 한다가 아닌
특정 컬럼을 어떻게 색인할 것인가로 접근하는게 맞는듯 보인다.
2. field추가가 자유롭다. 여느 nosql도 마찬가지겠지만, 루씬 역시, 특정 컬럼을 추가할 때 field를 추가하면된다.
3. 색인은 특정 값이 아닌 문장 단위의 것들로 한다. 물론 아닐경우도 있지만, 색인은 문장 자체를 설정된 분석기로 색인을 하는것이다.
이때문에 단순한 컬럼은 색인을 하지 않는다. 이유는 !! 검색엔진 이니까!
4. 검색 시 쿼리는 가공(?)을 해야한다. 물론 단순한 값을 할 경우는 필요없지만, lucene을 제대로 사용할려면, 검색 키워드를 가공을 하여 검색할 경우 더 큰 빚을 발휘 하는듯 보인다. ex) lucene 를 검색 시 lucening로 검색 하면 좋지 않을까? 머 이딴거.....
머 이정도 인듯 보인다.
위에 그림은 전형적인 루씬의 그림으로 index는 index document 과정이 필요하고, 검색 시는 search가 필요하다.
ps: 아마도 뒤에 lucene 관련 내용은 3.6 기반으로 설명할 것이다.(버전이 중요함.. ㅠㅠ)