이번에는 문자열을 검색하고 이를 highlight 처리하는 방법에 대해서 알아봅니다.
먼저 highlight 기능을 이용하려면 dependency에 highliter를 추가해야 합니다.
maven project의 pom.xml 파일에 하기와 같이 추가하면 됩니다.
Index writing
class DocumentWriter2 {
companion object {
const val INDEX_PATH = "/Users/myhome/workspace/LuceneTest/index2"
}
val indexWriter = makeIndexWriter()
fun createDoc(path: Path, attrs: BasicFileAttributes): Document {
return Document().apply {
add(StringField("path", path.toString(), Field.Store.YES))
add(TextField("content", File(path.toString()).readText(), Field.Store.YES))
add(LongPoint("size", attrs.size()))
add(StringField("size", attrs.size().toString(), Field.Store.YES))
}
}
private fun makeIndexWriter(): IndexWriter {
//Writer 생성
val dir = FSDirectory.open(Paths.get(INDEX_PATH))
val config = IndexWriterConfig(StandardAnalyzer())
return IndexWriter(dir, config)
}
}
2. Write main class 작성
fun main(args: Array<String>) {
// SourceFile 경로
val TARGET_PATH = "/Users/myhome/workspace/LuceneTest/searchTarget"
val docWriter = DocumentWriter2()
val path = Paths.get(TARGET_PATH)
// 일단 초기화
docWriter.indexWriter.deleteAll()
// nio 패키지를 이용란 파일 전체 순회 처리
Files.walkFileTree(path, object : SimpleFileVisitor<Path>() {
@Throws(IOException::class)
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
try {
//document 생성.
val doc = docWriter.createDoc(file, attrs)
//index 생성 -> path가 같을 경우 덮어쓰기.
docWriter.indexWriter.updateDocument(Term("path", file.toString()), doc)
} catch (e: IOException) {
e.printStackTrace()
}
return FileVisitResult.CONTINUE
}
})
docWriter.indexWriter.close()
}
Search and Highlight
import org.apache.lucene.analysis.standard.StandardAnalyzer
import org.apache.lucene.index.DirectoryReader
import org.apache.lucene.queryparser.classic.QueryParser
import org.apache.lucene.search.IndexSearcher
import org.apache.lucene.search.Query
import org.apache.lucene.search.highlight.*
import org.apache.lucene.store.FSDirectory
import java.nio.file.Paths
class FileSearch2 {
companion object {
const val INDEX_PATH = "/Users/myhome/workspace/LuceneTest/index2"
}
// reader lazy initialize
val reader: DirectoryReader by lazy {
val dir = FSDirectory.open(Paths.get(INDEX_PATH))
DirectoryReader.open(dir)
}
// search lazy initialize
private val searcher: IndexSearcher by lazy { IndexSearcher(reader) }
fun getDoc(docid: Int) = searcher.doc(docid)
fun search(query: Query) = searcher.search(query, 10)
fun getQuery(column: String, text: String): Query {
val qp = QueryParser(column, StandardAnalyzer())
return qp.parse(text)
}
// Highlighter 생성
fun getHighlighter(query: Query): Highlighter {
// 발견된 개수에 따른 점수환산 및 SpanQuery로 변경
val queryScorer = QueryScorer(query)
val highlightStartTag ="<b><span style='color: rgb(255, 0, 0)'>"
val highlightEndTag = "</span></b>"
val formatter = SimpleHTMLFormatter(highlightStartTag, highlightEndTag)
return Highlighter(formatter, queryScorer).apply {
// text를 동일한 크기의 text fragment로 쪼갠다.
textFragmenter = SimpleSpanFragmenter(queryScorer)
}
}
}
helper의 내용은 앞쪽의 다른 예제들과 다르지 않습니다.
DirectoryReader와 IndexSearcher는 kolin의 lazy keyword를 사용하여 lazy initialize 시켰습니다만, java로 구성시 그냥 메번 변수에 바로 할당하여 사용해도 상관없습니다. (java에서 lazy initialize를 구현하려면 좀더 수고스럽기도 하고, 꼭 필요하여 lazy 처리한건 아닙니다.)
QueryScorer
spanQuery는 찾아야 하는 글자들의 집합이나, 집합한 글자간의 거리, 순서등을 제한하여 검색할수 있는 query이며, 자세한 내용은 하기 링크에서 확인하시면 됩니다.
https://tourspace.tistory.com/241
SimpleHTMLFormatter
Highlight 구문을 표시할때 강조표시를 HTML로 합니다.
생성자 없이 사용할 경우 기본적으로 <b> 태크를 이용하여 bold 처리합니다.
코드에서는 bold 처리및 색상도 빨간색으로 표시합니다.
그외에도 Formatter를 상속받아 구현한 클래스로 GradientFormatter, GradientSpanFromatter가 있으며, 이 formatter 사용시 배경색을 지정할 수 있습니다.
SimpleSpanFragmenter
검색된 글자를 어떻게 잘라낼지를 setTextFragmenter() 함수를 이용하여 Highlighter에 설정합니다.
SimpleSpanFragmenter는 Fragmenter를 상속받아 구현한 클래스로 , text를 동일한 크기의 조각으로 잘라냅니다.
다만 span된 text는 자르지 않습니다.
만약 SimpleFragmenter를 사용한다면, 검색된 글자만 간추려서 간략하게 나타납니다.
fun main(args: Array<String>) {
val fileSearcher = FileSearch2()
// 검색할 정보
val field = "content"
val text = "lucene core project query"
// content에서 lucene 검색
val query = fileSearcher.getQuery(field, text)
val topDocs = fileSearcher.search(query)
println("searchCount = ${topDocs.totalHits}")
val highlighter = fileSearcher.getHighlighter(query)
topDocs.scoreDocs.forEach {
val document = fileSearcher.getDoc(it.doc)
println("file: ${document.get("path")} | " +
"size:${document.get("size")}")
// Highlight
val termVector = fileSearcher.reader.getTermVectors(it.doc)
val content = document.get(field)
val tokenStream = TokenSources.getTokenStream(field,termVector, content, StandardAnalyzer(), -1)
val fragments = highlighter.getBestFragments(tokenStream, content, 10)
fragments.forEach {
println("content: $it")
println("")
}
}
}
기본적으로 문자열을 검색하는 방법은 예전 예제와 동일합니다.IndexSearcher에 query를 넘겨 TopDocs를 반환받습니다.
TopDocs를 순회하면서 document를 얻어와 정보를 출력합니다.
이때 highlight를 하기 위해서 token stream을 만들어 highlighter에게 넘겨주면 highlight된 text fragments들을 얻을 수 있습니다.
Token Stream은 TokenSources의 getTokenStream() 함수를 이용하여 얻습니다.
getTokenStream()함수의 인자로 termVector (반환 타입 Fileds)정보를 넣어 줍니다.
이는 DirectoryReader의 getTermVectors()를 통해서 얻을수 있습니다.
다만 이 예제에서는 index를 write 할때 TextField로 만들어 넣었습니다.
TextField는 termVector를 생성하지 않는 field type 이므로 getTermVectors() 호출시 null이 반환됩니다. (따라서 이 예제에서는 해당 getTokenStream()의 termVector param을 null을 넣었어도 됩니다.)
또한 termVector param의 값이 null 이면 content param(검색 대상이 되는 전체 구문)을 이용하여 해당 문구를 다시 분석합니다.
결과는 아래와 같이나옵니다.
이쁘게 원하는 코드들이 잘 검색 됐네요~~
searchCount = 3 hits
file: /Users/1111336/workspace/LuceneTest/searchTarget/text1 | size:428
content: Lucene Core, our flagship sub-project, provides Java-based indexing and search technology, as well as spellchecking, hit highlighting and advanced analysis/tokenization capabilities. SolrTM is a high performance search server built using Lucene Core, with XML/HTTP and JSON/Python/Ruby APIs, hit highlighting, faceted search, caching, replication, and a web admin interface. PyLucene is a Python wrapper around the Core project.
file: /Users/1111336/workspace/LuceneTest/searchTarget/text3 | size:696
content: New Features New XYShape Field and Queries for indexing and querying general cartesian geometries. Snowball stemmer/analyzer for the Estonian language. Provide a FeatureSortfield to allow sorting search hits by descending value of a feature. Add new KoreanNumberFilter that can change Hangul character to number and process decimal point. Add doc-value support to range fields. Add monitor subproject (previously Luwak monitoring library) that allows a stream of documents to be matched against a set of registered queriesin an efficient manner. Add a numeric range query in sandbox that takes advantage of index sorting.Add a numeric range query in sandbox that takes advantage of index sorting.
file: /Users/1111336/workspace/LuceneTest/searchTarget/text2 | size:740
content: The Lucene project has added two new announce mailing lists, issues@lucene.apache.org and builds@lucene.apache.org. High-volume automated emails from our bug tracker, JIRA and GitHub will be moved from the dev@ list to issues@ and automated emails from our Jenkins CI build servers will be moved from the dev@ list to builds@. This is an effort to reduce the sometimes overwhelming email volume on our main development mailing list and thus make it easier for the community to follow important discussions by humans on the dev@lucene.apache.org list. Everyone who wants to continue receiving these automated emails should sign up for one or both of the two new lists. Sign-up instructions can be found on the Lucene-java and Solr web sites.
UnifiedHighlighter
먼저 상단에서 만들었던 Helper class의 최 하단에 UnifiedHighlighter를 반환하는 함수를 하나 추가합니다.
class FileSearch2 {
companion object {
const val INDEX_PATH = "/Users/myhome/workspace/LuceneTest/index2"
}
// reader lazy initialize
val reader: DirectoryReader by lazy {
...
}
// search lazy initialize
private val searcher: IndexSearcher by lazy { IndexSearcher(reader) }
fun getDoc(docid: Int) = searcher.doc(docid)
fun search(query: Query) = searcher.search(query, 10)
fun getQuery(column: String, text: String): Query {
...
}
// Highlighter 생성
fun getHighlighter(query: Query): Highlighter {
// 발견된 개수에 따른 점수환산 및 SpanQuery로 변경
...
}
fun getUnifiedHighlighter() = UnifiedHighlighter(searcher, StandardAnalyzer())
}
그리고 마찬가지로 main() 함수에서 기존 Highlighter를 주석 처리하고 UnifiedHighlighter를 사용하도록 추가 합니다.
fun main(args: Array) {
...
// val highlighter = fileSearcher.getHighlighter(query)
val highlighter = fileSearcher.getUnifiedHighliter()
topDocs.scoreDocs.forEach {
val document = fileSearcher.getDoc(it.doc)
println("file: ${document.get("path")} | " +
"size:${document.get("size")}")
// Highlight
// val termVector = fileSearcher.reader.getTermVectors(it.doc)
// val content = document.get(field)
// val tokenStream = TokenSources.getTokenStream(field, termVector, content, StandardAnalyzer(), -1)
// val fragments = highlighter.getBestFragments(tokenStream, content, 10)
//UnifiedHighlighter
val fragments = highlighter.highlight(field, query, topDocs)
fragments.forEach {
println("content: $it")
println("")
}
}
}
검색 결과는 아래와 같습니다.
searchCount = 3 hits
file: /Users/myhome/workspace/LuceneTest/searchTarget/text1 | size:428
content: PyLucene is a Python wrapper around the Core project.
content: Add a numeric range query in sandbox that takes advantage of index sorting.Add a numeric range query in sandbox that takes advantage of index sorting.
content: The Lucene project has added two new announce mailing lists, issues@lucene.apache.org and builds@lucene.apache.org.
file: /Users/myhome/workspace/LuceneTest/searchTarget/text3 | size:696
content: PyLucene is a Python wrapper around the Core project.
content: Add a numeric range query in sandbox that takes advantage of index sorting.Add a numeric range query in sandbox that takes advantage of index sorting.
content: The Lucene project has added two new announce mailing lists, issues@lucene.apache.org and builds@lucene.apache.org.
file: /Users/myhome/workspace/LuceneTest/searchTarget/text2 | size:740
content: PyLucene is a Python wrapper around the Core project.
content: Add a numeric range query in sandbox that takes advantage of index sorting.Add a numeric range query in sandbox that takes advantage of index sorting.
content: The Lucene project has added two new announce mailing lists, issues@lucene.apache.org and builds@lucene.apache.org.
Process finished with exit code 0
'개발이야기 > Lucene & Solr' 카테고리의 다른 글
[Lucene] 루씬 Indexing #2 - DocValues란? (0) | 2019.10.11 |
---|---|
[Lucene] 루씬 indexing #1 - 기본 (0) | 2019.10.10 |
[Lucene] 루씬 - spanQuery란? (SpanTermQuery, SpanNearQuery) (0) | 2019.10.04 |
[Lucene] 루씬 - 디렉토리(폴더) indexing 및 search 예제 (0) | 2019.10.04 |
[Lucene] 루씬 - Document Field types (0) | 2019.10.02 |