Apache Lucene을 사용한 대화의 전체 텍스트 검색: 자습서
게시 됨: 2022-03-11Apache Lucene은 문서의 전체 텍스트 검색에 사용되는 Java 라이브러리로, Solr 및 Elasticsearch와 같은 검색 서버의 핵심입니다. 또한 Android 앱 또는 웹 백엔드와 같은 Java 애플리케이션에 포함될 수도 있습니다.
Lucene의 구성 옵션은 광범위하지만 일반 텍스트 모음에서 데이터베이스 개발자가 사용하기 위한 것입니다. 문서에 특정 구조나 콘텐츠 유형이 있는 경우 둘 중 하나를 활용하여 검색 품질과 쿼리 기능을 개선할 수 있습니다.
이러한 종류의 사용자 정의의 예로, 이 Lucene 자습서에서 수천 개의 무료 전자 책을 제공하는 Project Gutenberg의 말뭉치를 색인화할 것입니다. 우리는 이 책들 중 많은 부분이 소설이라는 것을 알고 있습니다. 우리가 이 소설들 내의 대화 에 특히 관심이 있다고 가정해 봅시다. Lucene, Elasticsearch, Solr 모두 콘텐츠를 대화로 식별하는 즉시 사용 가능한 도구를 제공하지 않습니다. 사실, 그들은 텍스트 분석의 초기 단계에서 구두점을 버릴 것입니다. 이는 대화인 텍스트 부분을 식별할 수 있는 것과 반대입니다. 따라서 맞춤화를 시작해야 하는 초기 단계입니다.
Apache Lucene 분석 파이프라인의 일부
Lucene 분석 JavaDoc은 텍스트 분석 파이프라인에서 움직이는 모든 부분에 대한 좋은 개요를 제공합니다.
높은 수준에서 분석 파이프라인은 처음에는 원시 문자 스트림을 소비하고 끝에는 대략 단어에 해당하는 "용어"를 생성하는 것으로 생각할 수 있습니다.
표준 분석 파이프라인은 다음과 같이 시각화할 수 있습니다.
큰따옴표로 표시된 텍스트 영역을 인식하도록 이 파이프라인을 사용자 지정하는 방법을 살펴보겠습니다. 이 영역을 대화라고 하고 해당 영역에서 검색할 때 일치하는 항목을 추가합니다.
문자 읽기
문서가 색인에 처음 추가되면 Java InputStream에서 문자를 읽어 파일, 데이터베이스, 웹 서비스 호출 등에서 가져올 수 있습니다. Project Gutenberg의 색인을 생성하기 위해 전자책을 다운로드하고 이러한 파일을 읽고 인덱스에 쓰는 작은 응용 프로그램을 만듭니다. Lucene 인덱스를 생성하고 파일을 읽는 것은 잘 진행되는 경로이므로 많이 탐색하지 않겠습니다. 인덱스 생성을 위한 필수 코드는 다음과 같습니다.
IndexWriter writer = ...; BufferedReader reader = new BufferedReader(new InputStreamReader(... fileInputStream ...)); Document document = new Document(); document.add(new StringField("title", fileName, Store.YES)); document.add(new TextField("body", reader)); writer.addDocument(document);
각 전자책은 단일 Lucene Document
에 해당하므로 나중에 검색 결과는 일치하는 책 목록이 됩니다. Store.YES
는 파일 이름일 뿐인 제목 필드를 저장함을 나타냅니다. 그러나 검색할 때 필요하지 않고 디스크 공간만 낭비하므로 ebook의 본문 을 저장하고 싶지 않습니다.
스트림의 실제 읽기는 addDocument
로 시작됩니다. IndexWriter
는 파이프라인 끝에서 토큰을 가져옵니다. 이 풀은 첫 번째 단계인 Tokenizer
가 InputStream
에서 읽을 때까지 파이프를 통해 다시 진행됩니다.
또한 Lucene이 이를 처리하므로 스트림을 닫지 않습니다.
캐릭터 토큰화
Lucene StandardTokenizer는 구두점을 제거하므로 따옴표를 보존해야 하므로 여기에서 사용자 지정을 시작합니다.
StandardTokenizer
에 대한 문서는 소스 코드를 복사하고 필요에 맞게 조정하도록 초대하지만 이 솔루션은 불필요하게 복잡합니다. 대신 CharTokenizer
를 확장하여 "수락"할 문자를 지정할 수 있습니다. 여기서 "수락"되지 않은 문자는 토큰 사이의 구분 기호로 처리되어 폐기됩니다. 우리는 단어와 그 주변의 인용문에 관심이 있으므로 사용자 정의 Tokenizer는 간단합니다.
public class QuotationTokenizer extends CharTokenizer { @Override protected boolean isTokenChar(int c) { return Character.isLetter(c) || c == '"'; } }
[He said, "Good day".]
의 입력 스트림이 주어지면 생성된 토큰은 [He]
, [said]
, ["Good]
, [day"]
이 됩니다.
따옴표가 토큰 내에 산재되어 있는 방법에 유의하십시오. 각 견적에 대해 별도의 토큰을 생성하는 Tokenizer
를 작성하는 것이 가능하지만 Tokenizer
는 버퍼링 및 스캔과 같은 번거롭고 쉽게 망칠 수 있는 세부 정보에도 관심이 있으므로 Tokenizer
를 단순하게 유지하고 정리하는 것이 가장 좋습니다. 파이프라인을 따라 토큰 스트림을 추가합니다.
필터를 사용하여 토큰 분할
토크나이저 다음에는 일련의 TokenFilter
개체가 있습니다. TokenFilter
가 토큰을 추가, 제거 또는 수정할 수 있으므로 해당 필터 는 약간 잘못된 이름입니다.
Lucene에서 제공하는 많은 필터 클래스는 단일 단어를 예상하므로 단어와 인용 부호가 혼합된 토큰이 유입되지 않습니다. 따라서 Lucene 튜토리얼의 다음 사용자 정의는 QuotationTokenizer
의 출력을 정리할 필터의 도입이어야 합니다.
이 정리에는 인용문이 단어의 시작 부분에 나타나면 추가 시작 인용 토큰이 생성되고, 인용문이 끝에 나타나면 끝 인용 토큰이 생성됩니다. 단순함을 위해 작은 따옴표로 묶인 단어의 처리는 제쳐두겠습니다.
TokenFilter
하위 클래스를 만들려면 incrementToken
메서드를 구현해야 합니다. 이 메서드는 파이프의 이전 필터에서 incrementToken
을 호출한 다음 해당 호출의 결과를 조작하여 필터가 담당하는 작업을 수행해야 합니다. incrementToken
의 결과는 토큰 처리의 현재 상태를 설명하는 Attribute
개체를 통해 사용할 수 있습니다. incrementToken
반환을 구현한 후 속성이 조작되어 다음 필터(또는 파이프의 끝에 있는 경우 인덱스)에 대한 토큰을 설정해야 합니다.
파이프라인의 이 시점에서 관심 있는 속성은 다음과 같습니다.
CharTermAttribute
: 현재 토큰의 문자를 보유하는char[]
버퍼를 포함합니다. 견적을 제거하거나 견적 토큰을 생성하려면 이것을 조작해야 합니다.TypeAttribute
: 현재 토큰의 "유형"을 포함합니다. 토큰 스트림에 시작 및 끝 따옴표를 추가하기 때문에 필터를 사용하여 두 가지 새로운 유형을 소개합니다.OffsetAttribute
: Lucene은 선택적으로 원본 문서의 용어 위치에 대한 참조를 저장할 수 있습니다. 이러한 참조를 "오프셋"이라고 하며 원래 문자 스트림의 시작 및 끝 인덱스입니다. 토큰의 하위 문자열만 가리키도록CharTermAttribute
의 버퍼를 변경하는 경우 적절하게 이러한 오프셋을 조정해야 합니다.
토큰 스트림을 조작하기 위한 API가 왜 그렇게 복잡하고 특히 들어오는 토큰에 대해 String#split
과 같은 작업을 수행할 수 없는지 궁금할 수 있습니다. 이것은 Lucene이 내장된 토크나이저와 필터가 메가바이트의 메모리만 사용하면서 기가바이트의 텍스트를 빠르게 씹을 수 있는 고속, 낮은 오버헤드 인덱싱을 위해 설계되었기 때문입니다. 이를 달성하기 위해 토큰화 및 필터링 중에 할당이 거의 또는 전혀 수행되지 않으므로 위에서 언급한 Attribute
인스턴스는 한 번 할당되고 재사용되도록 되어 있습니다. 토크나이저와 필터가 이러한 방식으로 작성되고 자체 할당을 최소화하면 성능 저하 없이 Lucene을 사용자 정의할 수 있습니다.
이 모든 것을 염두에 두고 ["Hello]
와 같은 토큰을 사용하고 ["]
및 [Hello]
라는 두 개의 토큰을 생성하는 필터를 구현하는 방법을 살펴보겠습니다.
public class QuotationTokenFilter extends TokenFilter { private static final char QUOTE = '"'; public static final String QUOTE_START_TYPE = "start_quote"; public static final String QUOTE_END_TYPE = "end_quote"; private final OffsetAttribute offsetAttr = addAttribute(OffsetAttribute.class); private final TypeAttribute typeAttr = addAttribute(TypeAttribute.class); private final CharTermAttribute termBufferAttr = addAttribute(CharTermAttribute.class);
앞에서 본 속성 중 일부에 대한 참조를 얻는 것으로 시작합니다. 필드 이름에 "Attr"을 접미사로 붙여 나중에 참조할 때 명확해집니다. 일부 Tokenizer
구현은 이러한 속성을 제공하지 않을 수 있으므로 addAttribute
를 사용하여 참조를 가져옵니다. addAttribute
는 속성 인스턴스가 없으면 생성하고, 그렇지 않으면 해당 유형의 속성에 대한 공유 참조를 가져옵니다. Lucene은 한 번에 동일한 속성 유형의 여러 인스턴스를 허용하지 않습니다.
private boolean emitExtraToken; private int extraTokenStartOffset, extraTokenEndOffset; private String extraTokenType;
필터가 원래 스트림에 없는 새 토큰을 도입하기 때문에 incrementToken
호출 사이에 해당 토큰의 상태를 저장할 장소가 필요합니다. 기존 토큰을 두 개로 분할하기 때문에 새 토큰의 오프셋과 유형만 알면 충분합니다. 또한 incrementToken
에 대한 다음 호출이 이 추가 토큰을 방출할지 여부를 알려주는 플래그가 있습니다. Lucene은 실제로 한 쌍의 메서드인 captureState
및 restoreState
를 제공하여 이를 수행합니다. 그러나 이러한 방법에는 State
개체의 할당이 포함되며 실제로 해당 상태를 직접 관리하는 것보다 까다로울 수 있으므로 사용을 피하겠습니다.
@Override public void reset() throws IOException { emitExtraToken = false; extraTokenStartOffset = -1; extraTokenEndOffset = -1; extraTokenType = null; super.reset(); }
공격적인 할당 회피의 일환으로 Lucene은 필터 인스턴스를 재사용할 수 있습니다. 이 상황에서 reset
을 호출하면 필터를 초기 상태로 되돌릴 것으로 예상됩니다. 따라서 여기에서 추가 토큰 필드를 재설정하기만 하면 됩니다.
@Override public boolean incrementToken() throws IOException { if (emitExtraToken) { advanceToExtraToken(); emitExtraToken = false; return true; } ...
이제 우리는 흥미로운 부분에 도달하고 있습니다. incrementToken
구현이 호출되면 파이프라인의 초기 단계에서 incrementToken
을 호출 하지 않을 기회가 있습니다. 그렇게 함으로써 우리는 Tokenizer
에서 토큰을 가져오지 않기 때문에 새로운 토큰을 효과적으로 도입합니다.
대신, 우리는 추가 토큰의 속성을 설정하기 위해 advanceToExtraToken
을 호출하고, 다음 호출에서 이 분기를 피하기 위해 emitExtraToken
을 false로 설정한 다음, 다른 토큰을 사용할 수 있음을 나타내는 true
를 반환합니다.
@Override public boolean incrementToken() throws IOException { ... (emit extra token) ... boolean hasNext = input.incrementToken(); if (hasNext) { char[] buffer = termBufferAttr.buffer(); if (termBuffer.length() > 1) { if (buffer[0] == QUOTE) { splitTermQuoteFirst(); } else if (buffer[termBuffer.length() - 1] == QUOTE) { splitTermWordFirst(); } } else if (termBuffer.length() == 1) { if (buffer[0] == QUOTE) { typeAttr.setType(QUOTE_END_TYPE); } } } return hasNext; }
incrementToken
의 나머지 부분은 세 가지 다른 작업 중 하나를 수행합니다. termBufferAttr
은 파이프를 통해 들어오는 토큰의 내용을 검사하는 데 사용됩니다.
토큰 스트림의 끝에 도달하면(즉,
hasNext
가 false임) 완료되고 간단히 반환됩니다.하나 이상의 문자로 된 토큰이 있고 해당 문자 중 하나가 따옴표인 경우 토큰을 분할합니다.
토큰이 단독 인용이면 끝 인용이라고 가정합니다. 그 이유를 이해하기 위해 시작 따옴표는 항상 단어의 왼쪽에 나타나지만(즉, 중간 구두점 없음), 끝 따옴표는 구두점 뒤에 올 수 있습니다(예: 문장에서
[He told us to "go back the way we came."]
). 이 경우 종료 인용문은 이미 별도의 토큰이 되므로 유형만 설정하면 됩니다.
splitTermQuoteFirst
및 splitTermWordFirst
는 속성을 설정하여 현재 토큰을 단어 또는 따옴표로 만들고 나머지 절반이 나중에 소비될 수 있도록 "추가" 필드를 설정합니다. 두 가지 방법은 비슷하므로 splitTermQuoteFirst
만 살펴보겠습니다.
private void splitTermQuoteFirst() { int origStart = offsetAttr.startOffset(); int origEnd = offsetAttr.endOffset(); offsetAttr.setOffset(origStart, origStart + 1); typeAttr.setType(QUOTE_START_TYPE); termBufferAttr.setLength(1); prepareExtraTerm(origStart + 1, origEnd, TypeAttribute.DEFAULT_TYPE); }
이 토큰을 스트림에 먼저 나타나는 따옴표로 분할하고 싶기 때문에 길이를 1(즉, 한 문자, 즉 따옴표)로 설정하여 버퍼를 자릅니다. 그에 따라 오프셋을 조정하고(즉, 원본 문서의 인용문을 가리킴) 유형을 시작 인용문으로 설정합니다.
prepareExtraTerm
은 extra*
필드를 설정하고 emitExtraToken
을 true로 설정합니다. "추가" 토큰(즉, 따옴표 뒤에 오는 단어)을 가리키는 오프셋으로 호출됩니다.
전체 QuotationTokenFilter
는 GitHub에서 사용할 수 있습니다.
제쳐두고, 이 필터는 하나의 추가 토큰만 생성하지만 이 접근 방식은 임의의 수의 추가 토큰을 도입하도록 확장될 수 있습니다. extra*
필드를 컬렉션으로 바꾸거나 생성할 수 있는 추가 토큰 수에 제한이 있는 경우 고정 길이 배열로 바꾸세요. 이에 대한 예는 SynonymFilter
및 해당 PendingInput
내부 클래스를 참조하십시오.
견적 토큰 소비 및 표시 대화
이러한 인용문을 토큰 스트림에 추가하기 위한 모든 노력을 기울였으므로 이제 이를 사용하여 텍스트에서 대화 섹션을 구분할 수 있습니다.
최종 목표는 용어가 대화의 일부인지 여부에 따라 검색 결과를 조정하는 것이기 때문에 해당 용어에 메타데이터를 첨부해야 합니다. Lucene은 이를 위해 PayloadAttribute
를 제공합니다. 페이로드는 인덱스의 용어와 함께 저장되는 바이트 배열이며 나중에 검색하는 동안 읽을 수 있습니다. 이는 플래그가 전체 바이트를 낭비하게 차지하므로 추가 페이로드를 비트 플래그로 구현하여 공간을 절약할 수 있음을 의미합니다.

아래는 분석 파이프라인의 맨 끝에 추가된 새로운 필터인 DialoguePayloadTokenFilter
입니다. 토큰이 대화의 일부인지 여부를 나타내는 페이로드를 첨부합니다.
public class DialoguePayloadTokenFilter extends TokenFilter { private final TypeAttribute typeAttr = getAttribute(TypeAttribute.class); private final PayloadAttribute payloadAttr = addAttribute(PayloadAttribute.class); private static final BytesRef PAYLOAD_DIALOGUE = new BytesRef(new byte[] { 1 }); private static final BytesRef PAYLOAD_NOT_DIALOGUE = new BytesRef(new byte[] { 0 }); private boolean withinDialogue; protected DialoguePayloadTokenFilter(TokenStream input) { super(input); } @Override public void reset() throws IOException { this.withinDialogue = false; super.reset(); } @Override public boolean incrementToken() throws IOException { boolean hasNext = input.incrementToken(); while(hasNext) { boolean isStartQuote = QuotationTokenFilter .QUOTE_START_TYPE.equals(typeAttr.type()); boolean isEndQuote = QuotationTokenFilter .QUOTE_END_TYPE.equals(typeAttr.type()); if (isStartQuote) { withinDialogue = true; hasNext = input.incrementToken(); } else if (isEndQuote) { withinDialogue = false; hasNext = input.incrementToken(); } else { break; } } if (hasNext) { payloadAttr.setPayload(withinDialogue ? PAYLOAD_DIALOGUE : PAYLOAD_NOT_DIALOGUE); } return hasNext; } }
이 필터는 withinDialogue
상태의 단일 부분만 유지하면 되므로 훨씬 간단합니다. 시작 따옴표는 현재 대화 섹션 내에 있음을 나타내고 끝 따옴표는 대화 섹션이 종료되었음을 나타냅니다. 두 경우 모두 incrementToken
에 대한 두 번째 호출을 통해 견적 토큰이 삭제되므로 실제로 시작 견적 또는 종료 견적 토큰은 파이프라인의 이 단계를 지나치지 않습니다.
예를 들어 DialoguePayloadTokenFilter
는 토큰 스트림을 변환합니다.
[the], [program], [printed], ["], [hello], [world], ["]`
이 새 스트림으로:
[the][0], [program][0], [printed][0], [hello][1], [world][1]
토크나이저와 필터를 함께 묶기
Analyzer
는 일반적으로 Tokenizer
와 일련의 TokenFilter
를 결합하여 분석 파이프라인을 조립하는 역할을 합니다. Analyzer
는 분석 간에 파이프라인을 재사용하는 방법도 정의할 수 있습니다. Lucene이 항상 수행하는 사용 사이에 reset()
호출을 제외하고 구성 요소가 필요하지 않으므로 이에 대해 걱정할 필요가 없습니다. Analyzer#createComponents(String)
을 구현하여 어셈블리를 수행하기만 하면 됩니다.
public class DialogueAnalyzer extends Analyzer { @Override protected TokenStreamComponents createComponents(String fieldName) { QuotationTokenizer tokenizer = new QuotationTokenizer(); TokenFilter filter = new QuotationTokenFilter(tokenizer); filter = new LowerCaseFilter(filter); filter = new StopFilter(filter, StopAnalyzer.ENGLISH_STOP_WORDS_SET); filter = new DialoguePayloadTokenFilter(filter); return new TokenStreamComponents(tokenizer, filter); } }
앞에서 보았듯이 필터에는 파이프라인의 이전 단계에 대한 참조가 포함되어 있으므로 이를 인스턴스화합니다. 또한 StandardAnalyzer
의 몇 가지 필터( LowerCaseFilter
및 StopFilter
)를 삽입합니다. 이 두 개는 따옴표가 분리되었는지 확인하기 위해 QuotationTokenFilter
뒤에 와야 합니다. QuotationTokenFilter
이후의 어느 곳에서나 할 것이기 때문에 DialoguePayloadTokenFilter
배치에서 더 유연할 수 있습니다. 궁극적으로 제거될 불용어에 대화 페이로드를 주입하는 데 시간을 낭비하지 않도록 StopFilter
뒤에 둡니다.
다음은 작동 중인 새 파이프라인의 시각화입니다(제거했거나 이미 본 표준 파이프라인 부분 제외).
DialogueAnalyzer
는 이제 다른 주식 Analyzer
처럼 사용할 수 있으며 이제 인덱스를 구축하고 검색으로 이동할 수 있습니다.
대화의 전체 텍스트 검색
대화만 검색하려면 인용문 이외의 모든 토큰을 버릴 수 있었고 완료되었을 것입니다. 대신 원래 토큰을 모두 그대로 두어 대화를 고려한 쿼리를 수행하거나 대화를 텍스트의 다른 부분처럼 취급할 수 있는 유연성을 갖게 되었습니다.
Lucene 인덱스 쿼리의 기본 사항은 문서화되어 있습니다. 우리의 목적을 위해 쿼리가 MUST
또는 SHOULD
와 같은 연산자와 함께 결합된 Term
개체와 해당 용어를 기반으로 하는 일치 문서로 구성된다는 것을 아는 것으로 충분합니다. 그런 다음 일치하는 문서는 구성 가능한 Similarity
개체를 기반으로 점수가 매겨지며 이러한 결과는 점수, 필터링 또는 제한별로 정렬될 수 있습니다. 예를 들어 Lucene을 사용하면 [hello]
및 [world]
용어를 모두 포함해야 하는 상위 10개 문서에 대한 쿼리를 수행할 수 있습니다.
페이로드를 기반으로 문서의 점수를 조정하여 대화를 기반으로 검색 결과를 사용자 정의할 수 있습니다. 이에 대한 첫 번째 확장 지점은 Similarity
에 있으며 일치하는 용어의 가중치를 부여하고 점수를 매기는 역할을 합니다.
유사도 및 점수 매기기
쿼리는 기본적으로 문서에서 발생하는 빈도에 따라 용어에 가중치를 주는 DefaultSimilarity
를 사용합니다. 가중치를 조정하기 위한 좋은 확장 지점이므로 페이로드를 기반으로 문서의 점수를 매길 수도 있습니다. 이를 위해 DefaultSimilarity#scorePayload
메서드가 제공됩니다.
public final class DialogueAwareSimilarity extends DefaultSimilarity { @Override public float scorePayload(int doc, int start, int end, BytesRef payload) { if (payload.bytes[payload.offset] == 0) { return 0.0f; } return 1.0f; } }
DialogueAwareSimilarity
는 단순히 비대화 페이로드를 0으로 점수를 매깁니다. 각 Term
는 여러 번 일치될 수 있으므로 잠재적으로 여러 페이로드 점수를 갖게 됩니다. 이러한 점수의 해석은 Query
구현까지입니다.
페이로드를 포함하는 BytesRef
에 세심한 주의를 기울이십시오. 바이트 배열이 이전에 저장한 페이로드와 동일하다고 가정할 수 없기 때문에 offset
에서 바이트를 확인해야 합니다. 인덱스를 읽을 때 Lucene은 단순히 scorePayload
를 호출하기 위해 별도의 바이트 배열을 할당하는 데 메모리를 낭비하지 않으므로 기존 바이트 배열에 대한 참조를 얻습니다. Lucene API에 대해 코딩할 때 개발자의 편의성보다 성능이 우선이라는 점을 염두에 두는 것이 좋습니다.
이제 새로운 Similarity
구현이 있으므로 쿼리를 실행하는 데 사용되는 IndexSearcher
에서 설정해야 합니다.
IndexSearcher searcher = new IndexSearcher(... reader for index ...); searcher.setSimilarity(new DialogueAwareSimilarity());
쿼리 및 용어
이제 IndexSearcher
가 페이로드에 점수를 매길 수 있으므로 페이로드를 인식하는 쿼리도 구성해야 합니다. PayloadTermQuery
를 사용하여 단일 Term
를 일치시키면서 해당 일치 항목의 페이로드를 확인할 수도 있습니다.
PayloadTermQuery helloQuery = new PayloadTermQuery(new Term("body", "hello"), new AveragePayloadFunction());
이 쿼리는 본문 필드 내에서 [hello]
라는 용어와 일치합니다. 또한 모든 용어 일치에서 최종 페이로드 점수를 계산하는 함수를 제공해야 하므로 모든 페이로드 점수를 평균화하는 AveragePayloadFunction
을 연결합니다. 예를 들어 [hello]
라는 용어가 대화 내부에서 두 번, 대화 외부에서 한 번 발생하면 최종 페이로드 점수는 ²⁄₃가 됩니다. 이 최종 페이로드 점수는 전체 문서에 대해 DefaultSimilarity
에서 제공한 점수와 곱해집니다.
많은 용어가 대화 외부에 나타나는 검색 결과를 덜 강조하고 대화에 용어가 전혀 없는 문서에 대해 0점을 생성하기 위해 평균을 사용합니다.
대화에 포함된 여러 용어를 검색하려는 경우 BooleanQuery
를 사용하여 여러 PayloadTermQuery
개체를 구성할 수도 있습니다.
PayloadTermQuery worldQuery = new PayloadTermQuery(new Term("body", "world"), new AveragePayloadFunction()); BooleanQuery query = new BooleanQuery(); query.add(helloQuery, Occur.MUST); query.add(worldQuery, Occur.MUST);
이 쿼리가 실행되면 쿼리 구조와 유사성 구현이 함께 작동하는 방식을 볼 수 있습니다.
쿼리 실행 및 설명
쿼리를 실행하기 위해 IndexSearcher
에 전달합니다.
TopScoreDocCollector collector = TopScoreDocCollector.create(10); searcher.search(query, new PositiveScoresOnlyCollector(collector)); TopDocs topDocs = collector.topDocs();
Collector
개체는 일치하는 문서 컬렉션을 준비하는 데 사용됩니다.
수집기는 정렬, 제한 및 필터링의 조합을 달성하도록 구성될 수 있습니다. 예를 들어 대화에 하나 이상의 용어가 포함된 상위 10개 채점 문서를 얻으려면 TopScoreDocCollector
와 PositiveScoresOnlyCollector
를 결합합니다. 양수 점수만 취하면 점수가 0인 일치 항목(즉, 대화에 용어가 없는 항목)이 필터링됩니다.
이 쿼리가 작동하는지 확인하기 위해 실행한 다음 IndexSearcher#explain
을 사용하여 개별 문서의 점수가 어떻게 매겨졌는지 확인할 수 있습니다.
for (ScoreDoc result : topDocs.scoreDocs) { Document doc = searcher.doc(result.doc, Collections.singleton("title")); System.out.println("--- document " + doc.getField("title").stringValue() + " ---"); System.out.println(this.searcher.explain(query, result.doc)); }
여기에서 검색으로 얻은 TopDocs
의 문서 ID를 반복합니다. 또한 IndexSearcher#doc
를 사용하여 표시할 제목 필드를 검색합니다. "hello"
쿼리의 경우 결과는 다음과 같습니다.
--- Document whelv10.txt --- 0.072256625 = (MATCH) btq, product of: 0.072256625 = weight(body:hello in 7336) [DialogueAwareSimilarity], result of: 0.072256625 = fieldWeight in 7336, product of: 2.345208 = tf(freq=5.5), with freq of: 5.5 = phraseFreq=5.5 3.1549776 = idf(docFreq=2873, maxDocs=24796) 0.009765625 = fieldNorm(doc=7336) 1.0 = AveragePayloadFunction.docScore() --- Document daved10.txt --- 0.061311778 = (MATCH) btq, product of: 0.061311778 = weight(body:hello in 6873) [DialogueAwareSimilarity], result of: 0.061311778 = fieldWeight in 6873, product of: 3.3166249 = tf(freq=11.0), with freq of: 11.0 = phraseFreq=11.0 3.1549776 = idf(docFreq=2873, maxDocs=24796) 0.005859375 = fieldNorm(doc=6873) 1.0 = AveragePayloadFunction.docScore() ...
출력에 전문 용어가 포함되어 있지만 사용자 지정 Similarity
구현이 채점에 사용된 방식과 MaxPayloadFunction
이 이러한 일치에 대해 1.0
의 승수를 생성한 방식을 볼 수 있습니다. 이것은 페이로드가 로드되고 점수가 매겨지고 "Hello"
의 모든 일치가 대화에서 발생했음을 의미하므로 이러한 결과는 예상되는 상단에 바로 표시됩니다.
페이로드가 포함된 Project Gutenberg의 인덱스는 크기가 거의 4GB에 이르지만 내 겸손한 개발 시스템에서는 쿼리가 즉시 발생한다는 점을 지적할 가치가 있습니다. 검색 목표를 달성하기 위해 속도를 희생하지 않았습니다.
마무리
Lucene은 문자의 원시 스트림을 가져와 토큰으로 묶고 색인에서 용어로 유지하는 강력한 목적을 위해 구축된 전체 텍스트 검색 라이브러리입니다. 해당 인덱스를 빠르게 쿼리하고 순위가 매겨진 결과를 제공할 수 있으며 효율성을 유지하면서 확장을 위한 충분한 기회를 제공합니다.
Lucene을 응용 프로그램에서 직접 사용하거나 서버의 일부로 사용하면 기가바이트 콘텐츠에 대해 실시간으로 전체 텍스트 검색을 수행할 수 있습니다. 또한 사용자 지정 분석 및 점수 매기기를 통해 문서의 도메인별 기능을 활용하여 결과 또는 사용자 지정 쿼리의 관련성을 개선할 수 있습니다.
이 Lucene 자습서의 전체 코드 목록은 GitHub에서 사용할 수 있습니다. 리포지토리에는 인덱스 구축을 위한 LuceneQueryApp
과 쿼리 수행을 위한 LuceneIndexerApp
의 두 가지 애플리케이션이 포함되어 있습니다.
BitTorrent를 통해 디스크 이미지로 얻을 수 있는 Project Gutenberg의 말뭉치에는 읽을 가치가 있는 책이 많이 포함되어 있습니다(Lucene을 사용하거나 구식 방식으로).
즐거운 인덱싱!