일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- kubenetes
- 쿠버네티스
- H-index
- 스프링 스케쥴러
- 모던 자바 인 액션
- 기능개발
- 다리를 지나는 트럭
- 완주하지 못한 선수
- 전화번호 목록
- 크론 표현식
- 루씬 인 액션
- 정렬
- K번째수
- 프로그래머스
- 검색 기능 확장
- @configuration
- 고차원 함수
- @EnableScheduling
- 영속 자료구조
- @Data
- 스택/큐
- 코딩 테스트
- 커링
- @Getter
- 가장 큰 수
- 롬복 어노테이션
- 알고리즘
- @Setter
- Java
- 해시
- Today
- Total
Today I Learned
[루씬 인 액션] 5장. 고급 검색 기법 본문
5장에서 다루는 내용
- 문서의 필드 값 불러오기
- 검색 결과 필터링과 정렬
- 스팬 질의, 함수 질의
- 텀 벡터 활용
- 검색 중단
기본적인 기능을 뛰어넘은 검색이 필요할 때 사용할 고급 검색 기능들
- 각 결과 문서에서 질의에 해당하는 텀의 위치 정보를 세심하게 활용하는 고급 질의인 스팬 질의를 사용하는 방법
- 구문 질의에서 유사어를 검색할 수 있는 MultiPhraseQuery 클래스
- FieldSelector 클래스를 사용해 검색 결과 문서에서 원하는 필드만 불러오는 방법
- 여러 개의 루씬 색인을 대상으로 검색하는 방법
- 일정 시간이 지나면 진행중인 검색 작업을 중단하는 방법
- QueryParser에 기반을 둔 별도의 클래스를 사용해 여러 개의 필드를 한번에 검색하는 방법
5.1 필드 캐시
루씬의 역파일 색인 구조는 특정 텀을 포함하는 모든 문서를 조회하는 기능에 최적화된 상태
→ 모든 문서의 특정 필드 값을 조회하기는 어려움
색인에 저장한 모든 필드와 텀 벡터는 문서 ID로 조회할 수 있지만, 디스크에서 불러와야해서 속도 ↓
필드 캐시 : 특정 필드 값을 순차적으로 조회. 특정 필드 값을 기준으로 검색 결과를 정렬하는 기능 등에 사용
검색 도중 문서의 상세 정보를 데이터 저장소에서 불러올 수 있게 문서 ID를 조회해야하는 경우(문서마다 ID가 유일할때) 유용
ex) 1. 최근 문서일수록 중요도를 높게 지정하고자 한다면 해당 문서의 작성 일자 필드 값 필요
2. 제품마다 배송 무게가 별도로 지정되며, 검색 결과에서 각 제품의 예상 배송비를 보여줄 경우
5.1.1 모든 문서의 필드 값 불러오기
필드 캐시를 사용해 색인에 들어있는 모든 특정 필드의 값을 배열로 확보 가능
각 문서의 ID에 해당하는 배열 항목에 필드 값이 들어감
//모든 문서에 'weight' 필드가 있을때, 필드 캐시 배열 받기
float[] weights = FieldCache.DEFAULT.getFloats(reader, "weight");
특정 IndexReader와 필드에 대해 필드 캐시를 처음으로 사용 시,
해당 색인의 전체 문서 개수와 같은 크기의 대형 배열 생성 → 모든 문서의 해당 필드 값을 읽어 배열 저장
- 모든 문서의 필드 값을 읽기 때문에 생성은 느림
- 생성한 배열은 캐시로 동작하기 때문에 필드 캐시는 즉시 검색 가능
- 필드 캐시가 사용하는 메모리 양도 주의
5.1.2 세그먼트별 IndexReader
검색 결과를 찾아내고 정렬하는 기능은 모두 세그먼트 단위로 동작
즉, 필드 캐시를 생성할 때 루씬에서 필요한 IndexReader 인스턴스는 단 하나의 세그먼트를 대상으로 함
IndexReader를 다시 열때 변경된 세크먼트의 필드 캐시만 새로 만들기 때문에 성능 향상에 도움이 됨
5.2 검색 결과 정렬
기본설정(질의에 해당하는 문서 결과를 연관도 점수로 내림차순 정렬)이 아닌 다른 기준으로 정렬 시,
루씬에서 수집한 검색 결과를 모두 모아 검색 어플리케이션에서 직접 구현한 정렬기능을 사용한다면 속도가 너무 느림
이러한 요구사항에 대한 해결방법 및 루씬에서 제공하는 다양한 정렬 기법이 있음
5.2.1 필드 값으로 정렬
IndexSearcher.search(Query, Filter, int, Sort) : 정렬 기능을 추가한 검색 메소드
Sort 인자를 넘겨받아 검색하는 경우 검색 과정에서 문서의 연관도 점수를 계산 X
필드값 정렬에서 연관도 점수 필요 시,
IndexSearcher.setDefaultFieldSortScoring 메소드에서
doTrackScores 인자 - true : 모든 결과 문서의 연관도 점수를 계산
doMaxScores 인자 - true : 모든 결과 문서의 연관도 점수 중 최대값만 계산(작업량 더 많음)
예제 5.1 검색 결과를 필드 값으로 정렬
public class SortingExample {
private Directory directory;
public SortingExample(Directory directory) {
this.directory = directory;
}
public void displayResults(Query query, Sort sort) // 정렬하고자하는 sort 객체
throws IOException {
IndexSearcher searcher = new IndexSearcher(directory);
searcher.setDefaultFieldSortScoring(true, false); // 결과 건별로 연관도 점수 계산
TopDocs results = searcher.search(query, null, 20, sort); // Search 메소드 호출
System.out.println("\nResults for: " + // 정렬 관련 정보 출력
query.toString() + " sorted by " + sort);
System.out.println(StringUtils.rightPad("Title", 30) +
StringUtils.rightPad("pubmonth", 10) +
StringUtils.center("id", 4) +
StringUtils.center("score", 15));
PrintStream out = new PrintStream(System.out, true, "UTF-8"); //UTF-8 인코딩으로 화면에 출력할 검색결과 객체
DecimalFormat scoreFormatter = new DecimalFormat("0.######");
for (ScoreDoc sd : results.scoreDocs) {
int docID = sd.doc;
float score = sd.score;
Document doc = searcher.doc(docID);
out.println(
StringUtils.rightPad( //검색결과를 표 형태로..
StringUtils.abbreviate(doc.get("title"), 29), 30) +
StringUtils.rightPad(doc.get("pubmonth"), 10) +
StringUtils.center("" + docID, 4) +
StringUtils.leftPad(
scoreFormatter.format(score), 12));
out.println(" " + doc.get("category"));
}
searcher.close();
}
예제 5.2 여러 필드로 정렬한 결과 출력
public static void main(String[] args) throws Exception {
Query allBooks = new MatchAllDocsQuery();
/*테스트 질의 생성*/
QueryParser parser = new QueryParser(Version.LUCENE_30,
"contents",
new StandardAnalyzer(
Version.LUCENE_30));
BooleanQuery query = new BooleanQuery();
query.add(allBooks, BooleanClause.Occur.SHOULD);
query.add(parser.parse("java OR action"), BooleanClause.Occur.SHOULD);
/*예제 도서 색인 정보를 열고 SortingExample 인스턴스 생성 및 실행*/
Directory directory = TestUtil.getBookIndexDirectory();
SortingExample example = new SortingExample(directory);
example.displayResults(query, Sort.RELEVANCE);
example.displayResults(query, Sort.INDEXORDER);
example.displayResults(query, new Sort(new SortField("category", SortField.STRING)));
example.displayResults(query, new Sort(new SortField("pubmonth", SortField.INT, true)));
example.displayResults(query,
new Sort(new SortField("category", SortField.STRING),
SortField.FIELD_SCORE,
new SortField("pubmonth", SortField.INT, true)
));
example.displayResults(query, new Sort(new SortField[] {SortField.FIELD_SCORE, new SortField("category", SortField.STRING)}));
directory.close();
}
5.2.2 연관도 순서 정렬
Sort.RELEVANCE : 연관도 순 정렬(기본 설정 정렬)
정렬 결과
Sort 객체의 toString 메소드를 호출하면 <score>라고 출력되며, <score>는 연관도 점수 내림차순 정렬을 의미
정렬 조건이 동일할때는 문서 ID 순으로 정렬
5.2.3 색인 순서 정렬
Sort.INDEXORDER : 색인 순서 기준 정렬
전체 대상 문서를 한번에 색인하고 추후 변경하지 않는 경우 문서를 색인에 추가한 순서가 의미가 있음
문서를 추가하거나 재색인하는 등의 작업을 진행하면 새로 추가된 문서는 문서 ID를 새로 부여받음
정렬 결과
5.2.4 필드 값으로 정렬
new Sort(new SortField("category", SortField.STRING)) : 특정 필드 값 기준 정렬
필드 값 정렬을 사용하기 위에서는 해당 필드에 단 하나의 텀만 있어야함
정렬결과
분류 필드값 기준으로 사전 순서로 정렬됨
5.2.5 정렬 순서 변경
new Sort(new SortField("pubmonth", SortField.INT, true)) : 역방향을 뜻하는 인자에 true를 넣어주면 역방향 정렬
자연 정렬 순서는 연관도만 내림차순, 다른 필드는 모두 오름차순
정렬결과
출간일 기준으로 내림차순 정렬됨
출력 결과 중 상단에 정렬 조건을 보여주는 부분에서 맨 뒤의 느낌표(!)가 역방향 정렬을 의미
5.2.6 여러 필드의 값으로 정렬
똑같은 값이 많아 추가 정렬 기준이 필요할 경우, 여러 필드로 정렬 가능
new Sort(new SortField("category", sortField.STRING), SortField.FIELD_SCORE, new sortField("pubmonth", SortField.INT, true))
: category, 연관도 점수, pubmonth(역순) 순서로 정렬
SotrField 객체는 필드 이름과 필드의 자료형, 역방향 정렬 여부를 담고 있음
SCORE(연관도점수), DOC(문서ID), STRING, BYTE, SHORT, INT, LONG, FLOAT, DOUBLE 등의 자료형 지정 가능
정렬결과
5.2.7 정렬할 필드 자료형 선택
검색할 시점에는 정렬할 수 있는 필드와 해당 필드의 자료형이 이미 정해져있음
즉, 정렬에 필요한 내용은 검색하기 전의 색인 시점에 준비
5.2.8 정렬할 로케일 지정
SortField.STRING 자료형의 필드는 기본적으로 String.compareTo 메소드를 기준으로 정렬 순서 결정
원하는 로케일에 따라 정렬하고자 한다면, SortField 객체의 Locale 객체를 지정
public SortField (String field, Locale locale)
public SortField (String field, Locale locale, boolean reverse)
로케일은 숫자가 아닌 문자열에만 지정하기때문에 위 메소드는 모두 해당 필드를 문자열 형태로 정렬
5.3 MultiPhraseQuery 활용
MultiPhraseQuery는 기본적으로 PhraseQuery와 비슷하게 동작하지만, 한 위치에 여러 텀을 지정할 수 있음
MultiPhraseQuery를 사용하지않고 BooleanQuery로 구문을 연결해서 구현할 수도 있지만 성능 차이가 매우 큼
ex) quick 또는 fast 다음에 fox 단어가 나오는 문서를 찾을 때
"quick fox" OR "fast fox" 질의를 사용하거나, MultiPhraseQuery 질의를 사용
예제 5.3 MultiPhraseQuery 예제의 문서 색인
public class MultiPhraseQueryTest extends TestCase {
private IndexSearcher searcher;
protected void setUp() throws Exception {
Directory directory = new RAMDirectory();
IndexWriter writer = new IndexWriter(directory,
new WhitespaceAnalyzer(),
IndexWriter.MaxFieldLength.UNLIMITED);
Document doc1 = new Document();
doc1.add(new Field("field",
"the quick brown fox jumped over the lazy dog",
Field.Store.YES, Field.Index.ANALYZED));
writer.addDocument(doc1);
Document doc2 = new Document();
doc2.add(new Field("field",
"the fast fox hopped over the hound",
Field.Store.YES, Field.Index.ANALYZED));
writer.addDocument(doc2);
writer.close();
searcher = new IndexSearcher(directory);
}
예제 5.4 동일한 위치에 두개 이상의 텀을 추가하는 모습
public void testBasic() throws Exception {
MultiPhraseQuery query = new MultiPhraseQuery();
query.add(new Term[] { // 먼저 어느 텀이든 허용
new Term("field", "quick"),
new Term("field", "fast")
});
query.add(new Term("field", "fox")); // 그 후 단일 텀을 허용
System.out.println(query);
TopDocs hits = searcher.search(query, 10);
assertEquals("fast fox match", 1, hits.totalHits);
query.setSlop(1);
hits = searcher.search(query, 10);
assertEquals("both match", 2, hits.totalHits);
}
MultiPhraseQuery에도 PhraseQuery와 동일하게 슬롭 값을 지정 가능
예제 5.5 BooleanQuery로 MultiPhraseQuery와 동일한 기능을 구현한 모습
public void testAgainstOR() throws Exception {
PhraseQuery quickFox = new PhraseQuery();
quickFox.setSlop(1);
quickFox.add(new Term("field", "quick"));
quickFox.add(new Term("field", "fox"));
PhraseQuery fastFox = new PhraseQuery();
fastFox.add(new Term("field", "fast"));
fastFox.add(new Term("field", "fox"));
BooleanQuery query = new BooleanQuery();
query.add(quickFox, BooleanClause.Occur.SHOULD);
query.add(fastFox, BooleanClause.Occur.SHOULD);
TopDocs hits = searcher.search(query, 10);
assertEquals(2, hits.totalHits);
}
MultiQuery를 사용하면 슬롭 값이 모든 구문에 적용되지만,
PhraseQuery와 BooleanQuery를 사용하면 해당하는 PhraseQuery에만 슬롭 값이 적용됨
5.4 여러 개의 필드를 동시에 검색
필드에 관계없이 색인에 들어있는 모든 내용을 검색하고자 한다면, 모든 필드의 내용을 검색하는 세가지 방법을 사용할 수 있음
1. 모든 필드의 텍스트를 묶어 하나로 만든 별도의 필드를 준비하는 방법
Field 인스턴스를 여러개 사용하는 경우, 위치 증가값을 적절하게 지정해야 여러 필드 사이의 값이 혼동되지 않음
모든 필드의 값을 대상으로 검색할 때 전체 텍스트가 담겨있는 필드를 지정하기만 하면 됨
단점
- 필드별 중요도를 지정할 수 없음
- 개별 필드의 내용을 모두 색인한다면 색인이 저장되는 디스코 공간이 낭비됨
2. QueryParser 클래스를 상속받아 작성한 MultiFieldQueryParser를 사용하는 방법
MultiFieldQueryParser 내부적으로 QueryParser 인스턴스를 생성해 필드별 질의 표현식을 파싱한 후, BooleanQuery를 사용해 하나의 질의로 묶음
기본 설정으로 각 질의는 OR 연산자로 연결하며, 필요한 경우 필드마다 MUST, MUST_NOT, SHOULD 등의 연산자를 지정
단점
- MultiFieldQueryParser 클래스는 QueryParser 클래스를 사용하는 방법 때문에 사용 상 제약이 있음
날짜를 분석하는 로케일, 구문 질의를 생성할 때 사용하는 슬롭 값 등을 모두 기본 값만 사용
- 생성된 질의가 복잡함. 루씬에서 검색할때 각 질의어를 대상 필드에 하나씩 비교해야 하는데,
질의의 복잡도에 따라 모든 필드를 하나로 묶는 첫번째 방법에 비해 성능이 떨어질 가능성이 높음
3. 하나 또는 그 이상의 질으를 묶은 고급 질의인 DisjunctionMaxQuert 클래를 사용해 검색한 결과 문서를 하나로 합하는 방법
특정 문서가 하나 이상의 질의에 해당한다면, 해당하는 각 질의에 대해 계산한 점수 중 최고값을 해당 문서의 점수로 사용하기때문에
결과적으로 최종 사용자에게 좀 더 의미있는 연관도 점수가 될 수 있음
(BooleanQuery는 해당하는 질의에 대한 점수의 합을 문서의 점수로 사용)
tieBreakerMultiplier 인자를 지정하면 두 문서의 점수가 동일한 경우 더 많은 질의에 해당하는 문서가 더 높은 점수를 받기 때문에 두 개의 문서를 명확히 구분 가능
5.5 스팬 질의
스팬(span) : 특정 필드에서 토큰의 시작 위치와 끝 위치
분석 과정에서 생성된 토큰에는 직전 토큰과의 상대적인 위치를 나타내는 위치 증가값이 들어 있으며,
위치 증가값과 SpanQuery의 하위 클래스를 사용하면 훨씬 섬세한 질의를 검색 가능
ex) "president Obama" 구문이 "health care reform" 구문 근처에 있는 문서 검색
SpanTermQuery 질의 : TermQuery + 질의의 텀 위치 파악 (계산량 ↑)
5.5.1 SpanTermQuery
- 스팬 질의에서 시작하는 질의를 담당
- 문서마다 일치하는 모든 스팬의 시작과 끝 위치를 기억
예제 5.9 스팬 질의에 해당하는 모든 스팬을 화면에 출력하는 dumpSpans 메소드
private void dumpSpans(SpanQuery query) throws IOException {
Spans spans = query.getSpans(reader);
System.out.println(query + ":");
int numSpans = 0;
TopDocs hits = searcher.search(query, 10);
float[] scores = new float[2];
for (ScoreDoc sd : hits.scoreDocs) {
scores[sd.doc] = sd.score;
}
while (spans.next()) { // 스팬마다 확보
numSpans++;
int id = spans.doc();
Document doc = reader.document(id); // 해당문서 확보
TokenStream stream = analyzer.tokenStream("contents", // 원문을 다시 분석
new StringReader(doc.get("f")));
TermAttribute term = stream.addAttribute(TermAttribute.class);
StringBuilder buffer = new StringBuilder();
buffer.append(" ");
int i = 0;
while(stream.incrementToken()) { // 모든 토큰마다 반복
if (i == spans.start()) { // 스팬의 앞뒤에 <,> 기호 출력
buffer.append("<");
}
buffer.append(term.term());
if (i + 1 == spans.end()) {
buffer.append(">");
}
buffer.append(" ");
i++;
}
buffer.append("(").append(scores[id]).append(") ");
System.out.println(buffer);
}
if (numSpans == 0) {
System.out.println(" No spans");
}
System.out.println();
}
dumpSpans(brown) 출력결과
dumpSpans(new SpanQuery(new Term("f", "the))) 출력결과
5.5.2 필드의 맨 앞부분 검색
SpanFirstQuery : 필드의 맨 앞에서 일정 범위 안에 존재하는 스팬 검색
new SpanFirstQuery(brown, 2) - 검색 결과 X
new SpanFirstQuery(brown, 3) - 검색 성공
5.5.3 일정 범위 안에 위치한 스팬 검색
PhraseQuery : 가까이에 있는 텀을 찾아주며, 슬롭 값을 지정하면 앞뒤가 바뀐 문서도 검색 가능
SpanNearQuery : 일정 거리 안에 있는 스팬을 검색. 스팬이 지정된 순서대로 위치해야하는지, 앞뒤가 바뀌어도 되는지 지정 가능(inOrder 설정 - false)
5.5.4 겹치는 부분을 결과에서 제외
SpanNotQuery : SpanQuery와 겹치는 부분을 검색 결과에서 제거
- 첫번째 인자 : 결과에 포함하려는 스팬
- 두번째 인자 : 결과에서 제외하려는 스팬
테스트코드
public void testSpanNotQuery() throws Exception {
SpanNearQuery quick_fox =
new SpanNearQuery(new SpanQuery[]{quick, fox}, 1, true);
assertBothFoxes(quick_fox);
dumpSpans(quick_fox);
SpanNotQuery quick_fox_dog = new SpanNotQuery(quick_fox, dog);
assertBothFoxes(quick_fox_dog);
dumpSpans(quick_fox_dog);
SpanNotQuery no_quick_red_fox =
new SpanNotQuery(quick_fox, red);
assertOnlyBrownFox(no_quick_red_fox);
dumpSpans(no_quick_red_fox);
}
실행결과
5.5.5 SpanOrQuery
SpanOrQuery : SpanQuery의 배열을 하나로 묶음
5.5.6 스팬 질의와 QueryParser
QueryParser에서 SpanQuery 질의 생성은 X
루씬의 contrib 모듈의 서라운드 파서(surround parser)를 사용해 질의 생성 가능
5.6 검색 필터
검색 필터는 검색 대상을 줄여주며, 색인에 들어있는 문서 중 일부분만을 대상으로 검색
이전에 검색한 결과 안에서만 검색하거나, 기타 여러 상황에서 검색 대상을 제한하고자 할때 유용
5.6.1 TermRangeFilter
지정한 범위에 속한 텀을 포한하는 문서만 결과에 포함
TermRangeQuery 질의와 동일하지만 연관도 점수 계산 부분이 없음
범위를 무제한으로 설정 가능. 설정하려는 방향의 범위로 null 지정
new TermRangeFilter("modified", null, jan31, false, true);
혹은 Less 메소드나 More 메소드 사용
TermRangeFilter.Less("modified", jan31);
5.6.2 NumericRangeFilter
지정한 필드의 값이 지정한 숫자 범위 안에 포함되는 문서만 결과에 포함 NumericRangeQuery 질의와 동일하지만 연관도 점수 계산 부분이 없음
* NumericRangeQuery와 마찬가지로 precisionStep 인자로 별도의 값을 지정하려면 색인할 때 사용했던 precisionStep 값과 동일한 값으로 지정해야 함
5.6.3 FieldCacheRangeFilter
특정 텀이나 숫자 범위에 포함되는 문서만 결과에 포함성능을 높이고자 필드 캐시를 사용
FieldCacheRangeFilter.newStringRange("title2", "d", "j", true, true);
5.6.4 특정 텀으로 필터링
FieldCacheTermsFilter
특정 텀을 포함하는 문서만 결과에 포함. 필드 캐시 사용필드의 값으로 단 하나의 텀만 갖고있어야 함필드 캐시 최초 생성 시에는 시간이 많이 소요되며, 검색은 빠르게 처리 가능
TermsFilter
루씬 contrib 모듈 중 하나캐시를 사용하진 않지만, 필드 값으로 두개 이상의 텀을 갖고 있는 경우에도 사용 가능
5.6.5 QueryWrapperFilter
Query 인스턴스를 Filter로 바꿔주는 역할을 담당해당 Query의 결과 문서만을 필터의 대상 문서로 사용하며, 연관도 점수는 무시
질의를 실행한 결과를 바탕으로 필터를 구성한 다음, 다른 질의에 필터를 적용
TermQuery categoryQuery = new TermQuery(new Term("category", "/philosophy/eastern"));
Filter categoryFilter = new QueryWrapperFilter(categoryQuery);
5.6.6 SpanQueryFilter
SpanQuery 질의를 SpanFilter 클래스로 바꿔줌Filter 클래스를 상속받아 스팬 관련기능을 추가한 필터
QueryWrapperFilter와 비슷하지만 스팬 질의를 대상으로 한다는 점이 다름
SpanQuery categoryQuery = new SpanTermQuery(new Term("category", "/philosophy/eastern"));
Filter categoryFilter = new SpanQueryFilter(categoryQuery);
5.6.7 보안 필터
간단한 수준의 보안 요구사항은 사용자나 자격 등의 정보를 문서와 함께 색인한 다음 검색할 때 QueryWrapperFilter를 사용하여 구현 가능
5.6.8 필터와 BooleanQuery
검색 대상을 제한하는 질의를 Occur.MUST 조건으로 BooleanQuery에 추가하여 필터처럼 처리 가능필터를 사용하는 방법과 정규화된 연관도 점수가 크게 달라질 수 있음* BooleanQuery는 해당 질의의 텀을 포함하는 문서가 점수 계산 공식에 반영되지만, 필터를 사용하면 대상으로 삼는 문서의 개수가 달라지기 때문에 IDF(inverse document frequency) 값이 달라짐
BooleanQuery constrainedQuery = new BooleanQuery();
constrainedQuery.add(allBooks, BooleanClause.Occur.MUST);
constrainedQuery.add(categoryQuery, BooleanClause.Occur.MUST);
QueryParser를 사용해 질의를 생성 시, 사용자가 입력한 검색어로 생성한 질의와 대상 문서를 제한하는 질의를 BooleanQuery로 손쉽게 묶을 수 있어 유용함
5.6.9 PrefixFilter
특정 필드에서 원하는 접두어를 갖고있는 텀을 포함하는 문서만 검색 대상으로 지정하는 필터
PrfixQuery와 동일하지만 연관도 점수 계산 없음
new PrefixFilter(new Term("category", "/technology/computers"));
5.6.10 필터 캐시
필터를 사용하면 필터를 캐시하고 재사용할 수 있음
CachingWrapperFilter
어떤 종류의 필터든 캐시 가능
데코레이션 패턴의 형태로 다른 필터 객체를 감싸고 캐시
자주 사용하는 필터라면 CachingWrapperFilter를 통해 성능을 높일 수 있음
Filter filter = new TermRangeFilter("title2", "d", "j", true, true);
CachingWrapperFilter cachingFilter;
cachingFilter = new CachingWrapperFilter(filter);
CachingSpanFilter
CachingWrapperFilter와 동일하지만 SpanFilter 필터를 캐시한다는 점이 다름
5.6.11 필터를 질의로 변환
ConstantScoreQuery
필터를 질의로 변환
쿼리의 결과는 필터에 해당하는 문서만 포함하며, 질의에 지정한 중요도가 결과 문서의 연관도 점수로 지정
5.6.12 필터에 필터 적용
FilteredDocIdset
개별 문서 단위로 필터 정의
상속 받은 후 match 메소드를 직접 구현해야함
기본 필터를 지정해야 하며, 기본 필터에 속한 문서마다 match 메소드를 호출해 해당 문서를 걸러내야 하는지 아닌지를 판단
(원래 필터에 match 메소드로 한 단계 더 필터를 적용하는 셈)
일부 조건은 고정적이고 일부 조건은 실시간으로 검색 시점에 동적으로 확인해야하는 경우 유용
5.7 함수 질의와 연관도 점수
루씬 검색 질의 정렬 방법
1.연관도 점수 순서로 정렬(루씬 기본 설정)
2, 연관도 점수 대신 하나 또는 여러 개의 필드 값을 기준으로 정렬
3. 함수질의를 사용해 논리구조에 따라 해당하는 문서에 연관도 점수를 지정
5.7.1 함수 질의 클래스
ValueSourceQuery 클래스 : 모든 함수 질의의 최상위 클래스
ValueSource : 연관도 점수를 계산하는 객체. ValueSourceQuery 인스턴스 생성 시 지정
FieldCacheQuery : 색인된 특정 필드의 값으로 연관도 점수를 계산. 해당 필드가 norm 없이 단일 텀으로 색인한 숫자필드여야 함(Field.Index.NOT_ANALYZED_NO_NORMS)
Query q = new QueryParser(Version.LUCENE_30, //사용자의 검색어 표현식을 분석해
"content", //일반 질의를 생성
new StandardAnalyzer(
Version.LUCENE_30))
.parse("the green hat");
FieldScoreQuery qf = new FieldScoreQuery("score", //score 필드 값을 연관도 점수로 지정
FieldScoreQuery.Type.BYTE);
CustomScoreQuery customQ = new CustomScoreQuery(q, qf) {
public CustomScoreProvider getCustomScoreProvider(IndexReader r) {
return new CustomScoreProvider(r) { //원하는 공식으로 연관도 점수를 계산
public float customScore(int doc,
float subQueryScore,
float valSrcScore) {
return (float) (Math.sqrt(subQueryScore) * valSrcScore);
}
};
}
};
5.7.2 최근 문서에 중요도를 높게 부여하는 함수 질의
CustormScoreQuery : 문서의 중요도를 동적으로 지정하는 기능에 주로 사용
예제 5.15 최근 변경한 문서의 중요도를 높게 지정하는 RecencyBoostingQuery 함수 질의
static class RecencyBoostingQuery extends CustomScoreQuery {
double multiplier;
int today;
int maxDaysAgo;
String dayField;
static int MSEC_PER_DAY = 1000*3600*24;
public RecencyBoostingQuery(Query q, double multiplier,
int maxDaysAgo, String dayField) {
super(q);
today = (int) (new Date().getTime()/MSEC_PER_DAY);
this.multiplier = multiplier;
this.maxDaysAgo = maxDaysAgo;
this.dayField = dayField;
}
private class RecencyBooster extends CustomScoreProvider {
final int[] publishDay;
public RecencyBooster(IndexReader r) throws IOException {
super(r);
publishDay = FieldCache.DEFAULT // 필드 캐시에서
.getInts(r, dayField); // 날짜 값을 확보
}
public float customScore(int doc, float subQueryScore,
float valSrcScore) {
int daysAgo = today - publishDay[doc]; // 얼마나 오래됐는지 계산
if (daysAgo < maxDaysAgo) { // 일정 기간보다 오래된 항목은 무시
float boost = (float) (multiplier * // 간단한 공식으로 중요도 산정
(maxDaysAgo-daysAgo)
/ maxDaysAgo);
return (float) (subQueryScore * (1.0+boost));
} else {
return subQueryScore; // 중요도를 반영하지 않은 점수
}
}
}
public CustomScoreProvider getCustomScoreProvider(IndexReader r) throws IOException {
return new RecencyBooster(r);
}
}
예제 5.1.6 최근 문서의 중요도를 높게 지정하는 기능 테스트
public void testRecency() throws Throwable {
Directory dir = TestUtil.getBookIndexDirectory();
IndexReader r = IndexReader.open(dir);
IndexSearcher s = new IndexSearcher(r);
s.setDefaultFieldSortScoring(true, true);
QueryParser parser = new QueryParser(
Version.LUCENE_30,
"contents",
new StandardAnalyzer(
Version.LUCENE_30));
Query q = parser.parse("java in action"); // 검색어 표현식 파싱
Query q2 = new RecencyBoostingQuery(q, // RecencyBoostingQuery 인스턴스 생성
2.0, 2*365,
"pubmonthAsDay");
Sort sort = new Sort(new SortField[] {
SortField.FIELD_SCORE,
new SortField("title2", SortField.STRING)});
TopDocs hits = s.search(q2, null, 5, sort);
for (int i = 0; i < hits.scoreDocs.length; i++) {
Document doc = r.document(hits.scoreDocs[i].doc);
System.out.println((1+i) + ": " +
doc.get("title") +
": pubmonth=" +
doc.get("pubmonth") +
" score=" + hits.scoreDocs[i].score);
}
s.close();
r.close();
dir.close();
}
1. "java in action" 질의 생성
2. RecentBoostinhQuery 함수 질의 인스턴스 생성(최근 2년 안에 출간된 도서의 중요도를 2배 지정)
3. 첫번째 정렬조건-연관도 점수/두번째 정렬조건-제목 필드 지정
q 질의 실행
q2 질의 실행
5.8 다수의 루씬 색인 검색
다수의 루씬 색인을 유지하면서 전체 색인을 검색한 결과를 하나로 묶어 받아오는 기능이 필요할 때 사용
5.8.1 MultiSearcher
MultiSearcher 클래스 : MultiSearcher 안에 포함된 모든 색인을 대상으로 검색하고, 지정된 정렬 조건(기본 설정-연관도 내림차순)에 따라 정렬된 검색 결과 집합 하나를 받아옴.
IndexSearcher 인스턴스는 디렉토리를 지정했지만, MultiSearcher에는IndexSearcher배열을넘겨줘야함(데코레이션 패턴)
예제 5.17 MultiSEarcher로 두 개의 색인을 검색
public class MultiSearcherTest extends TestCase {
private IndexSearcher[] searchers;
public void setUp() throws Exception {
String[] animals = { "aardvark", "beaver", "coati",
"dog", "elephant", "frog", "gila monster",
"horse", "iguana", "javelina", "kangaroo",
"lemur", "moose", "nematode", "orca",
"python", "quokka", "rat", "scorpion",
"tarantula", "uromastyx", "vicuna",
"walrus", "xiphias", "yak", "zebra"};
Analyzer analyzer = new WhitespaceAnalyzer();
Directory aTOmDirectory = new RAMDirectory(); // 디렉토리 두개 생성
Directory nTOzDirectory = new RAMDirectory(); //
IndexWriter aTOmWriter = new IndexWriter(aTOmDirectory,
analyzer,
IndexWriter.MaxFieldLength.UNLIMITED);
IndexWriter nTOzWriter = new IndexWriter(nTOzDirectory,
analyzer,
IndexWriter.MaxFieldLength.UNLIMITED);
for (int i=animals.length - 1; i >= 0; i--) {
Document doc = new Document();
String animal = animals[i];
doc.add(new Field("animal", animal, Field.Store.YES, Field.Index.NOT_ANALYZED));
if (animal.charAt(0) < 'n') {
aTOmWriter.addDocument(doc); // 알파벳에 따라 두 개의
} else {
nTOzWriter.addDocument(doc); // 색인으로 구분해 추가
}
}
aTOmWriter.close();
nTOzWriter.close();
searchers = new IndexSearcher[2];
searchers[0] = new IndexSearcher(aTOmDirectory);
searchers[1] = new IndexSearcher(nTOzDirectory);
}
public void testMulti() throws Exception {
MultiSearcher searcher = new MultiSearcher(searchers);
TermRangeQuery query = new TermRangeQuery("animal", // 양쪽 색인에 걸치게 검색
"h",
"t",
true, true);3
TopDocs hits = searcher.search(query, 10);
assertEquals("tarantula not included", 12, hits.totalHits);
}
}
1. 알파멧 a~m 까지의 동물 색인, n~z 까지의 동물 색인 생성
2. 양쪽 색인을 대상으로 h~t 까지의 동물 검색
5.8.2 스레드를 활용하는 ParallelMultiSearcher
ParallelMultiSearcher : MultiSearcher와 같은 기능은 갖고 있지만 스레드를 활용해 검색
각 Searchable 인스턴스마다 스레드를 생성해 실행하고 검색이 모두 끝날 때까지 기다렸다가 결과를 취합해 하나의 결과로 리턴
5.9 텀 벡터 활용
텀 벡터
- 문서 내부로 한정된 역파일 색인
- 텀 빈도수의 쌍이며, 추가적으로 각 텀의 위치 정보를 담고 있을 수 있음
특정 문서의 필드에 대한 텀 벡터를 가져오려면 IndexReader 메소드 호출
TermFreqVector termFreqVector = reader.getTermFreqVector(id, "subject");
TermFreqVector
- 텀 벡터 관련 정보를 알려주는 여러가지 메소드를 포함
- String(텀 자체)과 int(텀의 빈도수) 값으로 구성된 배열을 가져오는 메소드가 가장 중요
5.9.1 비슷한 책 조회
텀 벡터를 사용해 특정 문서와 '비슷한' 문서를 찾아내는 기능을 구현할 수 있음
에제 5.18 특정 책과 비슷한 책을 찾아주는 예제코드
public class BooksLikeThis {
public static void main(String[] args) throws IOException {
Directory dir = TestUtil.getBookIndexDirectory();
IndexReader reader = IndexReader.open(dir);
int numDocs = reader.maxDoc();
BooksLikeThis blt = new BooksLikeThis(reader);
for (int i = 0; i < numDocs; i++) { // 모든 책을 하나씩 반복
System.out.println();
Document doc = reader.document(i);
System.out.println(doc.get("title"));
Document[] docs = blt.docsLike(i, 10); // 현재 책과 비슷한 책을 조회
if (docs.length == 0) {
System.out.println(" None like this");
}
for (Document likeThisDoc : docs) {
System.out.println(" -> " + likeThisDoc.get("title"));
}
}
reader.close();
dir.close();
}
private IndexReader reader;
private IndexSearcher searcher;
public BooksLikeThis(IndexReader reader) {
this.reader = reader;
searcher = new IndexSearcher(reader);
}
public Document[] docsLike(int id, int max) throws IOException {
Document doc = reader.document(id);
String[] authors = doc.getValues("author");
BooleanQuery authorQuery = new BooleanQuery(); // 동일한 저자의
for (String author : authors) { // 책에 가산점
authorQuery.add(new TermQuery(new Term("author", author)), //
BooleanClause.Occur.SHOULD); //
}
authorQuery.setBoost(2.0f);
TermFreqVector vector = // 'subject'
reader.getTermFreqVector(id, "subject"); // 텀 벡터에서
BooleanQuery subjectQuery = new BooleanQuery(); // 찾아낸 텀으로
for (String vecTerm : vector.getTerms()) { // 질의 준비
TermQuery tq = new TermQuery( //
new Term("subject", vecTerm));
subjectQuery.add(tq, BooleanClause.Occur.SHOULD); //
}
BooleanQuery likeThisQuery = new BooleanQuery(); // 최종 질의 완성
likeThisQuery.add(authorQuery, BooleanClause.Occur.SHOULD); //
likeThisQuery.add(subjectQuery, BooleanClause.Occur.SHOULD); //
likeThisQuery.add(new TermQuery( // 현재 책은
new Term("isbn", doc.get("isbn"))), BooleanClause.Occur.MUST_NOT); // 결과에서 제외
// System.out.println(" Query: " +
// likeThisQuery.toString("contents"));
TopDocs hits = searcher.search(likeThisQuery, 10);
int size = max;
if (max > hits.scoreDocs.length) size = hits.scoreDocs.length;
Document[] docs = new Document[size];
for (int i = 0; i < size; i++) {
docs[i] = reader.document(hits.scoreDocs[i].doc);
}
return docs;
}
}
1. 도서 정보 색인의 모든 책마다 그와 비슷한 책을 찾아 화면에 출력
2. 현재 문서와 비슷한 책을 찾아 배열로 받음
3. 동일한 저자의 책의 중요도를더 높게 지정
- 원본 데이터의 저자 부분은 쉼표로 구분한 문자열이었으나,
색인할 때는 문자열얼 Field 인스턴스로 구분해서 문서에 추가
4. subject 필드의 텀 벡터에서 받아온 텀을 BooleanQuery 질의 안에 검색어로 추가
5. 저자와 주제 질의를 하나의 질의로 통합
6. 현재 책은 제외하고 검색
위 예제는 텀 벡터를 지정된 필드에 속한 텀을 받아오는 방법으로 사용
텀 벡터가 없는 상황에서 주제 필드에 해당하는 텀을 모두 받아오려면 해당 필드의 내용을 다시 분석해야함
5.9.2 자동분류
텀 벡터를 활용해 가장 적절한 분류를 찾을 수 있음
예제 5.19 분류별 대표 벡터 생성
색인된 모든 문서를 하나씩 읽어가며 subject 필드의 텀 벡터를 분류마다 하나로 취합
private void buildCategoryVectors() throws IOException {
IndexReader reader = IndexReader.open(TestUtil.getBookIndexDirectory());
int maxDoc = reader.maxDoc();
for (int i = 0; i < maxDoc; i++) {
if (!reader.isDeleted(i)) {
Document doc = reader.document(i);
String category = doc.get("category");
Map vectorMap = (Map) categoryMap.get(category);
if (vectorMap == null) {
vectorMap = new TreeMap();
categoryMap.put(category, vectorMap);
}
TermFreqVector termFreqVector =
reader.getTermFreqVector(i, "subject");
addTermFreqToMap(vectorMap, termFreqVector);
}
}
분류별 대표 벡터Map< Key(분류 이름): value(Map<key(텀):value(빈도수)>)> 형태
예제 5.20 추가된 문서의 텀 벡터를 대표 벡터에 합산
private void addTermFreqToMap(Map vectorMap,
TermFreqVector termFreqVector) {
String[] terms = termFreqVector.getTerms();
int[] freqs = termFreqVector.getTermFrequencies();
for (int i = 0; i < terms.length; i++) {
String term = terms[i];
if (vectorMap.containsKey(term)) {
Integer value = (Integer) vectorMap.get(term);
vectorMap.put(term,
new Integer(value.intValue() + freqs[i]));
} else {
vectorMap.put(term, new Integer(freqs[i]));
}
}
}
벡터 공간에서 새로 추가하는 책의 분류는 해당 책의 subject 텀 벡터와 가장 각도가 가까운 대표 벡터의 분류로 결정
예제 5.21 가장 근접한 벡터를 찾아 분류를 결정
private String getCategory(String subject) {
String[] words = subject.split(" ");
Iterator categoryIterator = categoryMap.keySet().iterator();
double bestAngle = Double.MAX_VALUE;
String bestCategory = null;
while (categoryIterator.hasNext()) {
String category = (String) categoryIterator.next();
// System.out.println(category);
double angle = computeAngle(words, category);
// System.out.println(" -> angle = " + angle + " (" + Math.toDegrees(angle) + ")");
if (angle < bestAngle) {
bestAngle = angle;
bestCategory = category;
}
}
return bestCategory;
}
토큰 필터를 사용했다면 String.split 대신 subject 필드의 내용을 동일한 분석기에 입력해 토큰을 분리해야 정상 동작
예제 5.22 새로 추가한 책과 기존 분류의 대표 벡터 간의 각도 계산
private double computeAngle(String[] words, String category) {
Map vectorMap = (Map) categoryMap.get(category);
int dotProduct = 0;
int sumOfSquares = 0;
for (String word : words) {
int categoryWordFreq = 0;
if (vectorMap.containsKey(word)) {
categoryWordFreq =
((Integer) vectorMap.get(word)).intValue();
}
dotProduct += categoryWordFreq; //word 배열에는 중복 단어가 없다고 가정
sumOfSquares += categoryWordFreq * categoryWordFreq;
}
double denominator;
if (sumOfSquares == words.length) {
denominator = sumOfSquares; // 두 값이 일치하는 경우 제곱근 등을 사용하지않게 단순화
} else {
denominator = Math.sqrt(sumOfSquares) *
Math.sqrt(words.length);
}
double ratio = dotProduct / denominator;
return Math.acos(ratio);
}
/*
#1 Assume each word has frequency 1
#2 Shortcut to prevent precision issue
*/
}
문서와 문서, 문서와 대표 벡터 등 두 벡터 간의 각도를 구하는 일은 많은 연산 작업이 소요
제곱근 또는 역 코사인 함수 등이 문제가 되며, 규묘가 큰 색인에서는 성능에 영향을 주기도 함
5.9.3 TermVectorMapper
TermVectorMapper : Term 이 아닌 원하는 순서에 맞춰 정렬하고자 할때 또는 관심있는 몇 개의 텀만 불러오길 원할 때 사용
5.10 FieldSelector로 필드 선택
IndexReader를 사용해 색인에 들어있는 문서를 읽어올 수 있지만,(Field.Store.YES로 설정된 필드의 문서들)
검색 문서 양이 많거나 색인에 보관하는 필드의 양이 많을수록 성능이 떨어짐
FieldSelector : 본문 없이 제목과 저자 등의 메타 정보만 읽어오고자 할때 색인에서 문서마다 원하는 필드만 읽어올 수 있음
FieldSelector를 사용해 저장된 필드를 불러오면 IndexReader는 해당 문서의 필드를 색인된 순서대로 읽어가며 필드마다 FieldSelector를 확인해 필드의 원문을 불러올지, 어떤 상태로 불러올지 결정
5.11 검색 중단
TimeLimitingCollector 클래스 : 지정된 시간이 지나면 TimeExceedException 예외를 발생시켜 검색을 중단
public class TimeLimitingCollectorTest extends TestCase {
public void testTimeLimitingCollector() throws Exception {
Directory dir = TestUtil.getBookIndexDirectory();
IndexSearcher searcher = new IndexSearcher(dir);
Query q = new MatchAllDocsQuery();
int numAllBooks = TestUtil.hitCount(searcher, q);
TopScoreDocCollector topDocs = TopScoreDocCollector.create(10, false);
Collector collector = new TimeLimitingCollector(topDocs, // 기존의 Collector 인스턴스를
1000); // 그대로 사용
try {
searcher.search(q, collector);
assertEquals(numAllBooks, topDocs.getTotalHits()); // 결과 개수 확인
} catch (TimeExceededException tee) { // 제한 시간을 초과했다는 메시지
System.out.println("Too much time taken."); //
} //
searcher.close();
dir.close();
}
}
주의점
결과를 수집할 때 약간의 추가적인 부하가 있을 수 있음
검색 결과를 수집하는 도중에만 제한된 시간을 확인하기 때문에 Query.rewrite() 메소드의 소요시간 등은 계산하지 못함
'루씬 Lucene' 카테고리의 다른 글
[루씬 인 액션] 8장. 필수 확장 기능 (0) | 2021.02.09 |
---|---|
[루씬 인 액션] 6장. 검색 기능 확장 (0) | 2021.01.26 |
[루씬 인 액션] 4장. 루씬의 텍스트 분석 (0) | 2020.12.08 |
[루씬 인 액션] 3장. 검색 (0) | 2020.11.23 |
[루씬 인 액션] 2장. 색인 (0) | 2020.11.10 |