일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- H-index
- 크론 표현식
- @EnableScheduling
- @configuration
- 쿠버네티스
- 롬복 어노테이션
- @Data
- 가장 큰 수
- 프로그래머스
- 모던 자바 인 액션
- 알고리즘
- 전화번호 목록
- 고차원 함수
- 다리를 지나는 트럭
- Java
- 완주하지 못한 선수
- 정렬
- 기능개발
- 스택/큐
- 커링
- @Getter
- @Setter
- 해시
- 검색 기능 확장
- 영속 자료구조
- 루씬 인 액션
- K번째수
- kubenetes
- 코딩 테스트
- 스프링 스케쥴러
- Today
- Total
Today I Learned
[루씬 인 액션] 3장. 검색 본문
3장에서 다루는 내용
- 루씬 색인의 문서 검색
- 다양한 루씬 내장 질의 활용
- 검색 결과 활용
- 연관도 점수 계산 방법
- 사람이 입력한 질의 변환
- 루씬 색인에서 질의 실행
- ScoreDoc 객체의 목록이 담긴 TopDocs 인스턴스 생성
- (ScoreDoc 객체 목록은 연관도 점수 기준으로 내림차순 정렬 상태)
- 검색 질의를 기준으로 각 문서마다 점수 계산
- 사용자에게 보여줄 첫 번째 결과 페이지 문서 가져옴
3.1 간단한 검색 기능 구현
질의 생성 방식
1. 프로그램에서 직접 Query 객체 생성 - 강력한 검색 기능 활용, 자유로운 사용자 인터페이스 구현 가능
2. QueryParser 이용 - 규칙에 맞춰 텍스트 질의를 입력해야하지만 구현이 쉬움
3.1.1 텀 검색
IndexSearcher 클래스
- 색인 내용 검색 시 가장 핵심이 되는 클래스. 여러 종류의 search 메소드를 포함
- IndexSearcher 객체는 생성 시, 색인 정보를 읽어와 내부적으로 검색에 필요한 기본 자료 구조를 구성
- 검색 질의가 들어올때마다 IndexSearch를 열면 성능이 크게 떨어지므로, 일반적으로는 IndexSearcher와 Directory 객체를 항상 열어두고 검색 질의를 처리
예제 3.1 TermQuery로 검색
public class BasicSearchingTest extends TestCase {
public void testTerm() throws Exception {
Directory dir = TestUtil.getBookIndexDirectory(); //색인이 저장된 Directory
IndexSearcher searcher = new IndexSearcher(dir); //IndexSearcher 객체 생성
Term t = new Term("subject", "ant");
Query query = new TermQuery(t);
TopDocs docs = searcher.search(query, 10);
assertEquals("Ant in Action", //ant 검색어에 대한 결과 확인
1, docs.totalHits);
t = new Term("subject", "junit");
docs = searcher.search(new TermQuery(t), 10);
assertEquals("Ant in Action, " + //Junit 검색어에 대한
"JUnit in Action, Second Edition", //결과가 두건 있는지 확인
2, docs.totalHits);
searcher.close();
dir.close();
}
}
* 원본 텍스트는 분석 과정을 거치며 정규화됐을 가능성이 높다. 따라서 색인 과정에서 사용했던 분석기와 동일한 분석기로 검색해야 한다.
3.1.2 QueryParser로 사용자가 입력한 검색어 파싱
파싱(parsing) : 사용자가 입력한 텍스트 질의를 분석해 동일한 의미의 Query 객체로 변환하는 작업
예제 3.2 QueryParser를 사용해 텍스트 형태의 검색어를 Query 객체로 손쉽게 변환
public void testQueryParser() throws Exception {
Directory dir = TestUtil.getBookIndexDirectory();
IndexSearcher searcher = new IndexSearcher(dir);
QueryParser parser = new QueryParser(Version.LUCENE_30, // Query Parser 생성
"contents",
new SimpleAnalyzer());
Query query = parser.parse("+JUNIT +ANT -MOCK"); //텍스트 파싱
TopDocs docs = searcher.search(query, 10);
assertEquals(1, docs.totalHits);
Document d = searcher.doc(docs.scoreDocs[0].doc);
assertEquals("Ant in Action", d.get("title"));
query = parser.parse("mock OR junit"); //텍스트 파싱
docs = searcher.search(query, 10);
assertEquals("Ant in Action, " +
"JUnit in Action, Second Edition",
2, docs.totalHits);
searcher.close();
dir.close();
}
QueryParser 활용
QueryParser = new QueryParser(Version matchVersion, // 루씬 버전 지정(버전에 따른 하위 호환성 확보용)
String field, // 검색어 텍스트의 필드 이름
Analyzer analyzer) // 분석기
public Query parser(String query) throws ParseException
// query 문자열 인자로 간단한 질의 조회 가능
// 파싱 중 오류 발생 시 ParseException 발생
QueryParser 클래스로 기본 질의 처리
3.2 IndexSearcher 활용
3.2.1 IndexSearcher 인스턴스 생성
Directory dir = FSDirectory.open(new File("/path/to/index")); //추상화된 FS 형태의 API
IndexReader reader = IndexReader.open(dir); //API를 통해 실제 색인 파일 불러옴
IndexSearcher searcher = new IndexSearcher(reader); //검색 메소드를 제공
* IndexReader 인스턴스를 새로 만들 때 자원을 많이 소모하므로 최대한 재사용 필요하다.
하지만 인스턴스 생성 시점의 색인을 기준으로 하기 때문에, 변경된 내용이 즉각 검색에 노출되진 않는다.
3.2.2 검색 실행
IndexSearcher 클래스의 search 메소드 중 호출
> 색인에 들어 있는 모든 문서 조회 > 질의에 해당하는 문서만 검색 > 결과 중 첫번째 페이지 return
3.2.3 TopDocs 결과 활용
TopDocs 클래스
totalHits 속성 -검색 결과의 개수. 검색 결과는 연관도 점수를 기준으로 내림차순 정렬
ScoreDocs 배열 - Search 메소드에서 요청했던 개수에 해당하는 결과 문서 객체. float형 연관도 점수, int형 문서ID 포함
getMaxScore() - 전체 결과 중 가장 높은 점수
3.2.4 결과 페이지 이동
페이지 이동 기능 구현 방법
1. 최초 검색 시 여러 페이지의 검색 결과를 보관
2. 사용자가 페이지를 이동할 때마다 새로 검색
- 대부분의 경우 매번 새로 검색하는 것이 효율적
- ScoreDoc 배열과 IndexSearcher를 사용자마다 보관하려면 자원이 많이 소모
- 매번 새로 검색하는 일이 비효율적이라고 생각될 수 있지만, 일반적으로 루씬의 검색 속도가 충분하여 무리가 없음
- 대부분의 운영체제에서 지원하는 입출력 캐시 등으로 인해 관련 색인 파일 등이 캐시에 들어가 있을 확률이 높음
- 사용자가 두번째 페이지로 넘어가는 일이 많지 않음
(페이지 이동 횟수가 비정상적으로 많다면 검색 에플리케이션의 설계 구조를 다시 고려해봐야 함)
3.2.5 준실시간 검색
검색을 담당하는 부분과 IndexWriter가 같은 JVM안에 있다면 준실시간 검색 활용 가능
색인의 변경 사항을 처리하고 검색에 반영하는 최소한의 시간은 필요(실시간 검색 X)
추가하였지만 아직 commit하지 않은 문서까지 검색 가능
예제 3.3 준실시간 검색
public class NearRealTimeTest extends TestCase {
public void testNearRealTime() throws Exception {
Directory dir = new RAMDirectory();
IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(Version.LUCENE_30),
IndexWriter.MaxFieldLength.UNLIMITED);
for(int i=0;i<10;i++) {
Document doc = new Document();
doc.add(new Field("id", ""+i, Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
doc.add(new Field("text", "aaa", Field.Store.NO, Field.Index.ANALYZED));
writer.addDocument(doc);
}
IndexReader reader = writer.getReader(); // 준실시간 IndexReader
IndexSearcher searcher = new IndexSearcher(reader); // IndexSearcher 인스턴스 생성
Query query = new TermQuery(new Term("text", "aaa"));
TopDocs docs = searcher.search(query, 1);
assertEquals(10, docs.totalHits); // 검색결과 10건 확보
writer.deleteDocuments(new Term("id", "7")); // 문서 1건 삭제
Document doc = new Document(); // 문서 1건 추가
doc.add(new Field("id",
"11",
Field.Store.NO,
Field.Index.NOT_ANALYZED_NO_NORMS));
doc.add(new Field("text",
"bbb",
Field.Store.NO,
Field.Index.ANALYZED));
writer.addDocument(doc);
IndexReader newReader = reader.reopen(); // IndexReader 다시열기
assertFalse(reader == newReader);
reader.close(); // 기존 IndexReader 닫음
searcher = new IndexSearcher(newReader);
TopDocs hits = searcher.search(query, 10); // 결과가 9건인지 확인
assertEquals(9, hits.totalHits);
query = new TermQuery(new Term("text", "bbb")); // 새로 추가한 문서가
hits = searcher.search(query, 1); // 포함되었는지 확인
assertEquals(1, hits.totalHits);
newReader.close();
writer.close();
}
}
3.3 연관도 점수
3.3.1 점수 계산
유사도 점수는 질의(q), 텀(t), 문서(d)를 기준으로 계산
3.3.2 explain() 메소드로 점수 내역 확인
IndexSearcher.explain() - 내부적으로 점수 계산에 관여했던 모든 항목에 대한 상세 내역을 담은 Explanation 객체를 return
3.4 다양한 종류의 질의
3.4.1 TermQuery 텀 검색
텀 - 색인에 들어있는작은 단위. 필드의 이름 + (텍스트 형태의)값
Term t = new Term("contents", "java");
Query query = new TermQuery(t);
TopDocs docs = searcher.search(query, 10);
assertEquals("JUnit in Action, Second Edition", 1, docs.totalHits);
3.4.2 TermRangeQuery 텀 범위 검색
- Term은 색인 안에서 알파벳 순서로 정렬된 상태이므로, 원하는 범위의 텀을 손쉽게 뽑아낼 수 있음
- 시작 또는 종료에 질의 대신 null을 지정하면 해당 방향으로는 범위 제한 없이 검색
3.4.3 NumericRangeQuery 숫자 범위 검색
- 숫자가 색인된 트라이 구조와 같은 형태로 질의의 범위를 변환
3.4.4 PrefixQuery 접두어 검색
- 지정한 문자열로 시작하는 모든 텀을 갖고 있는 문서를 검색
3.4.5 BooleanQuery 불리언 질의
- AND, OR, NOT 등의 논리 구조를 구현하여 다양한 종류의 질의(절)를 적용
- BooleanClause.Occur.MUST / BooleanClause.Occur.SHOULD / BooleanClause.Occur.MUST_NOT
- 기본적으로 추가할 수 있는 최대 절 수 : 1024개
3.4.6 PhraseQuery 구문 검색
- 텀의 위치 정보를 활용해 원하는 텀이 일정 거리 안에 존재하는 문서를 검색
- 슬롭(slop) : 텀 사이의 최대 거리(구문을 원래 순서대로 맞출 때 움직여야 하는 이동거리)
다중 텀 구문
- 여러 개의 텀을 지정해도 슬롭 값은 항상 문서와 같은 순서로 배열할 때 필요한 이동 횟수의 최댓값
assertFalse("not close enough", matched(new String[] {"quick", "jumped", "lazy"}, 3));
구문 질의 점수 계산
- 지정된 구문을 문서의 문장에 맞추려 할 때 필요한 이동 거리를 기반으로 점수를 계산
- 구문점수 = 1 / 이동거리
3.4.7 WildcardQuery 와일드카드 검색
- 일반적으로 사용하는 와일드카드 문자를 사용해서 검색 가능
3.4.8 FuzzyQuery 비슷한 단어 검색
- 레벤스타인 거리 알고리즘을 사용해 색인의 단어와 질의의 단어 사이의 거리를 계산하고 문자열 사이의 유사도를 확인
- 편집 거리(edit distance)라고도 부르며, 첫 번째 문자열에 글자 삭제, 추가, 대치 등의 작업을 적용해 두번 째 문자열로 변환하는데 필요한 작업의 수를 의미
3.4.9 MatchAllDocsQuery 모든 문서 조회
- 색인에 들어있는 모든 문서를 검색 결과로 가져옴
- 문서의 특정 필드의 중요도를 유사도 점수로 사용하여 정렬 가능
3.5 QueryParser로 질의 표현식 파싱
- 사람이 쉽게 알아볼 수 있는 텍스트 형태의 질의 표현식을 기반으로 Query 객체 생성
- 검색 엔진에서 일상적으로 사용하는 다양한 검색어 지정 문법을 대부분 인식해 처리 가능
3.5.1 Query.toString
- Query를 상속받아 구현한 모든 질의 클래스는 toString() 메소드를 구현
- Query 객체 디버깅 또는 표현식 -> QueryParser의 변환 과정 확인 시 유용
- toString(String field) : field 인자로 해당 질의의 기본 필드 이름을 지정
- toString() : 비어있는 기본 필드 이름을 사용. 결과에는 모든 텀에 필드 선택 표시가 들어감
public void testToString() throws Exception {
BooleanQuery query = new BooleanQuery();
query.add(new FuzzyQuery(new Term("field", "kountry")), BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term("title", "western")), BooleanClause.Occur.SHOULD);
assertEquals("both kinds", "+kountry~0.5 title:western", query.toString("field"));
}
3.5.2 TermQuery
- 검색어 표현식의 단일 단어를 TermQuery로 변환(특수문자 등으로 다중 텀 질의에 포함되면 제외)
- Queryparser 생성 시 지정했던 기본 필드 이름을 질의 표현식에 적용해서 쿼리 생성
public void testTermQuery() throws Exception {
QueryParser parser = new QueryParser(Version.LUCENE_30, "subject", analyzer);
Query query = parser.parse("computers");
System.out.println("term: " + query);
}
//출력결과 : term:subject:computers
3.5.3 텀 범위 검색
-문자열 또는 날짜 등의 범위 검색 시 괄호 사용 / 시작 텀과 종료 텀 사이에 TO 구문(대문자) 지정
- 대괄호 [] : 해당 방향의 끝값 포함
- 중괄호 {} : 해당 방향의 끝값 미포함
(양쪽 방향의 포함여부 다르게 설정 불가)
3.5.4 숫자와 날짜 범위 검색
- NumericField 색인 여부를 저장하지 않으며, NumericRangeQuery도 사용 불가
- 날짜로 검색은 가능하지만 NumericField면 제대로 동작하지않음
3.5.5 접두어 질의와 와일드카드 질의
- 텀에 * 또는 ?가 포함되어있으면 wildcardQuery로 변환 (* 기호가 단어 뒤에만 있으면 prefixQuery)
- 접두어 질의와 와일드카드 질의 모두 텀을 소문자로 변환
- 단어의 맨 앞에는 와일드카드 사용 불가(QueryParser의 기본 설정) -> 성능이 급격히 떨어짐
- 써야한다면 setAllowLeadingWildcard 메소드 사용
3.5.6 불리언 연산자
- AND, OR, NOT 연산자(대문자)
- 연산자 미지정 시 기본 연산자로 OR 사용
//기본 연산자 AND 지정 방법
QueryParser parser = new QueryParser(Version.LuCENE_30, "contents", analyzer);
parser.setDefaultOperator(QueryParser.AND_OPERATOR);
- NOT 연산자 : 해당 텀을 포함하는 문서를 검색 결과에서 제외 -> NOT이 아닌 다른 연산자 지정 텀까지 있어야 검색 가능
3.5.7 구문 질의
- 큰따옴표 안에 있는 문자열은 분석기로 분석 절차를 거침(QueryParser의 결과가 질의 표현식과 다를 수 있음)
ex) "This is Some Phrase*" → StandardAnalyzer 분석 → 'Some Phrase' 구문 질의 생성(불용어 this, is 제거)
(와일드카드보다 구문의 우선순위가 높아 * 기호 무시됨)
- 불용어가 제거된 위치는 ? 글자로 표현되며, QueryParser.setPhraseSlop 메소드로 슬롭 값 지정 가능(~숫자)
3.5.8 퍼지 검색
- ~ 기호를 텀 뒤에 붙이면 퍼지 질의 생성("~x"는 구문질의, ""가 없어야 퍼지 질의)
- 최소 유사도 임계값을 실수 형태로 지정
- 와일드카드와 동일한 성능 저하있음
public void testFuzzyQuery() throws Exception {
QueryParser parser = new QueryParser(Version.LUCENE_30, "subject", analyzer);
Query query = parser.parse("kountry~");
System.out.println("fuzzy: " + query);
query = parser.parse("kountry~0.7");
System.out.println("fuzzy 2: " + query);
}
//출력결과
//fuzzy: subject:kounty~0.5
//fuzzy 2: subject:kounty~0.7
3.5.9 MatchAllDocsQuery
- *:* 문자열을 입력하면 MatchAllDocsQuery 질의 생성
3.5.10 질의 그룹
- 불리언 질의를 사용해 중첩된 질의 생성 가능
- 소괄호를 사용해 하위 계층의 질의를 표현
Query query = new QueryParser(
Version.LUCENE_30
, "subject"
, analyzer).parse("(agile OR extreme) AND methodology");
3.5.11 필드 선택
- QueryParser 인스턴스 생성 시 지정한 이름은 기본값. 실제 질의의 필드 이름은 제한 X
- 사용자가 직접 검색어 별로 필드 이름을 지정 가능. ex) title:lucene
3.5.12 하위 질의에 중요도 지정
- 캐럿(^) 문자 다음에 실수를 지정하면 검색어에 중요도 지정 가능. ex) junit^2.0 testing
3.5.1.3 과연 QueryParser를 사용해야 하는가?
- QueryParser는 사용자가 직접 복잡한 질의를 명시할 수 있게 해주지만, 질의 성능과 검색 애플리케이션의 정책 및 목적이 고려되어야함
- 가능한 모든 경우의 수가 예측되어야하며, 추가적인 제어 기능이 필요할 수도 있음
'루씬 Lucene' 카테고리의 다른 글
[루씬 인 액션] 6장. 검색 기능 확장 (0) | 2021.01.26 |
---|---|
[루씬 인 액션] 5장. 고급 검색 기법 (0) | 2021.01.10 |
[루씬 인 액션] 4장. 루씬의 텍스트 분석 (0) | 2020.12.08 |
[루씬 인 액션] 2장. 색인 (0) | 2020.11.10 |
[루씬 인 액션] 1장. 루씬과의 만남 (0) | 2020.11.03 |