使用 Lucene 搜索
发布日期:2024-11-25 03:30PM (最后更新:2024-12-22 08:45AM)
Groovy 博客文章经常引用其他 Apache 项目。也许我们想知道涉及了哪些其他项目和哪些博客文章。鉴于这些页面已发布,我们可以使用诸如 Apache Nutch 或 Apache Solr 之类的工具来抓取/索引这些网页并使用这些工具进行搜索。对于这篇文章,我们将从原始源 (AsciiDoc) 文件中搜索我们所需的信息,这些文件可以在 groovy-website 仓库中找到。我们将首先研究如何使用正则表达式查找项目引用,然后使用 Apache Lucene。
项目名称的正则表达式
为了本篇文章的需要,我们假设项目引用将包含“Apache”一词,后跟项目名称。为了更有趣,我们还将包含对 Eclipse 项目的引用。我们还将为包含子项目的项目做出规定,至少对于 Apache Commons,这样就可以识别诸如“Apache Commons Math”之类的名称。我们将排除 Apache Groovy,因为它可能会命中每一篇 Groovy 博客文章。我们还将排除一些在常用短语中出现的单词,例如“Apache License”和“Apache Projects”,这些短语在我们的搜索查询中可能看起来像项目名称,但实际上不是。
这绝不是一个完美的名称引用查找器,例如,我们经常在博客文章中首次引入 Apache Commons Math 时使用其全名,但在文章后面,我们会回退到更友好的“Commons Math”引用,其中“Apache”是从上下文中理解的。我们可以使正则表达式更复杂以适应这种情况,但就本篇文章而言,这并没有任何好处,所以我们不会这样做。
String tokenRegex = /(?ix) # ignore case, enable whitespace & comments
\b # word boundary
( # start capture of all terms
( # capture project name term
(apache|eclipse)\s # foundation name
(commons\s)? # optional subproject name
(
?!(groovy # negative lookahead for excluded words
| and | license | users
| https | projects | software
| or | prefixes | technologies)
)\w+
) # end capture project name term
| # alternatively
( # capture non-project term
\w+?\b # non-greedily match any other word chars
) # end capture non-project term
) # end capture term
/
我们使用了 Groovy 的多行斜线字符串,这样就无需转义反斜杠。我们还启用了正则表达式空格和注释来解释正则表达式。如果您愿意,可以随意创建一个紧凑的(长)无注释单行表达式。
使用正则表达式匹配收集项目名称统计信息
有了正则表达式,我们来看看如何使用 Groovy 匹配器来查找所有项目名称。首先,我们将定义另一个常用常量,即我们博客的根目录,如果您想继续并运行这些示例,可能需要更改它。
String baseDir = '/projects/groovy-website/site/src/site/blog' // (1)
-
你需要签出 Groovy 网站,并在此处将 `baseDir` 指向它。
首先,让我们创建一个小助手方法,用于打印漂亮的成果图(我们将使用 JColor 中的 `colorize` 方法)
def display(Map<String, Integer> data, int max, int scale = 1) {
data.each { k, v ->
var label = "$k ($v)"
var color = k.startsWith('apache') ? MAGENTA_TEXT() : BLUE_TEXT()
println "${label.padRight(32)} ${colorize(bar(v * scale, 0, max, max), color)}"
}
}
现在,我们的脚本将遍历该目录中的所有文件,使用我们的正则表达式处理它们并跟踪我们找到的命中数。
var histogram = [:].withDefault { 0 } // (1)
new File(baseDir).traverse(nameFilter: ~/.*\.adoc/) { file -> // (2)
var m = file.text =~ tokenRegex // (3)
var projects = m*.get(2).grep()*.toLowerCase()*.replaceAll('\n', ' ') // (4)
var counts = projects.countBy() // (5)
if (counts) {
println "$file.name: $counts" // (6)
counts.each { k, v -> histogram[k] += v } // (7)
}
}
println "\nFrequency of total hits mentioning a project:"
display(histogram.sort { e -> -e.value }, 50) // (8)
-
这是一个为不存在的键提供默认值的映射
-
这将遍历目录,处理每个 AsciiDoc 文件
-
我们定义了匹配器
-
这将提取项目名称(捕获组 2),忽略其他单词(使用 grep),转换为小写,并删除换行符,以防术语可能跨行末尾
-
这汇总了该文件的命中计数
-
我们打印出每个博客文章的文件名及其项目引用
-
我们将文件聚合添加到整体聚合中
-
我们打印出漂亮的 ASCII 条形图,总结了总体聚合。
当我们的脚本运行时,输出如下所示:
apache-nlpcraft-with-groovy.adoc: [apache nlpcraft:5] classifying-iris-flowers-with-deep.adoc: [eclipse deeplearning4j:5, apache commons math:1, apache spark:2] community-over-code-eu-2024.adoc: [apache ofbiz:1, apache commons math:2, apache ignite:1, apache spark:1, apache wayang:1, apache beam:1, apache flink:1] community-over-code-na-2023.adoc: [apache ignite:8, apache commons numbers:1, apache commons csv:1] deck-of-cards-with-groovy.adoc: [eclipse collections:5] deep-learning-and-eclipse-collections.adoc: [eclipse collections:7, eclipse deeplearning4j:2] detecting-objects-with-groovy-the.adoc: [apache mxnet:12] fruity-eclipse-collections.adoc: [eclipse collections:9, apache commons math:1] fun-with-obfuscated-groovy.adoc: [apache commons math:1] groovy-2-5-clibuilder-renewal.adoc: [apache commons cli:2] groovy-graph-databases.adoc: [apache age:11, apache hugegraph:3, apache tinkerpop:3] groovy-haiku-processing.adoc: [eclipse collections:3] groovy-list-processing-cheat-sheet.adoc: [eclipse collections:4, apache commons collections:3] groovy-lucene.adoc: [apache nutch:1, apache solr:1, apache lucene:3, apache commons:4, apache commons math:2, apache spark:1] groovy-null-processing.adoc: [eclipse collections:6, apache commons collections:4] groovy-pekko-gpars.adoc: [apache pekko:4] groovy-record-performance.adoc: [apache commons codec:1] handling-byte-order-mark-characters.adoc: [apache commons io:1] lego-bricks-with-groovy.adoc: [eclipse collections:6] matrix-calculations-with-groovy-apache.adoc: [apache commons math:6, eclipse deeplearning4j:1, apache commons:1] natural-language-processing-with-groovy.adoc: [apache opennlp:2, apache spark:1] reading-and-writing-csv-files.adoc: [apache commons csv:2] set-operations-with-groovy.adoc: [eclipse collections:3] solving-simple-optimization-problems-with-groovy.adoc: [apache commons math:5, apache kie:1] using-groovy-with-apache-wayang.adoc: [apache wayang:9, apache spark:7, apache flink:1, apache commons csv:1, apache ignite:1] whiskey-clustering-with-groovy-and.adoc: [apache ignite:7, apache wayang:1, apache spark:2, apache commons csv:2] wordle-checker.adoc: [eclipse collections:3] zipping-collections-with-groovy.adoc: [eclipse collections:4] Frequency of total hits mentioning a project: eclipse collections (50) ██████████████████████████████████████████████████▏ apache commons math (18) ██████████████████▏ apache ignite (17) █████████████████▏ apache spark (14) ██████████████▏ apache mxnet (12) ████████████▏ apache wayang (11) ███████████▏ apache age (11) ███████████▏ eclipse deeplearning4j (8) ████████▏ apache commons collections (7) ███████▏ apache commons csv (6) ██████▏ apache nlpcraft (5) █████▏ apache pekko (4) ████▏ apache hugegraph (3) ███▏ apache tinkerpop (3) ███▏ apache lucene (3) ███▏ apache flink (2) ██▏ apache commons cli (2) ██▏ apache commons (2) ██▏ apache opennlp (2) ██▏ apache ofbiz (1) █▏ apache beam (1) █▏ apache commons numbers (1) █▏ apache nutch (1) █▏ apache solr (1) █▏ apache commons codec (1) █▏ apache commons io (1) █▏ apache kie (1) █▏
使用 Lucene 索引
好的,正则表达式并不难,但通常我们可能想搜索很多东西。像 Lucene 这样的搜索框架可以帮助解决这个问题。让我们看看将 Lucene 应用到我们的问题中会是什么样子。
首先,我们将定义一个自定义分析器。Lucene 非常灵活,并内置了分析器。在典型情况下,我们可能只对所有找到的单词进行索引。为此有一个内置分析器。如果使用其中一个内置分析器,要查询我们的项目名称,我们需要构建一个跨越多个(单词)术语的查询。我们稍后将研究它可能是什么样子,但出于我们小示例的目的,我们假设项目名称是不可分割的术语,并以这种方式切分我们的文档。
幸运的是,Lucene 有一个模式分词器,这使我们能够重用现有的正则表达式。基本上,我们的索引将包含项目名称词项和其他找到的词。
class ProjectNameAnalyzer extends Analyzer {
@Override
protected TokenStreamComponents createComponents(String fieldName) {
var src = new PatternTokenizer(~tokenRegex, 0)
var result = new LowerCaseFilter(src)
new TokenStreamComponents(src, result)
}
}
现在让我们对文档进行分词并让 Lucene 对它们进行索引。
var analyzer = new ProjectNameAnalyzer() // (1)
var indexDir = new ByteBuffersDirectory() // (2)
var config = new IndexWriterConfig(analyzer)
new IndexWriter(indexDir, config).withCloseable { writer ->
var indexedWithFreq = new FieldType(stored: true,
indexOptions: IndexOptions.DOCS_AND_FREQS,
storeTermVectors: true)
new File(baseDir).traverse(nameFilter: ~/.*\.adoc/) { file ->
file.withReader { br ->
var document = new Document()
document.add(new Field('content', br.text, indexedWithFreq)) // (3)
document.add(new StringField('name', file.name, Field.Store.YES)) // (4)
writer.addDocument(document)
}
}
}
-
这是我们基于正则表达式的分析器
-
我们将为我们的小例子使用内存索引
-
存储文档内容以及词项位置信息
-
同时存储文件名
定义索引后,我们通常会执行某种搜索。我们很快就会这样做,但首先,对于我们感兴趣的信息类型,Lucene API 的一部分允许我们探索索引。以下是我们可能这样做的方式:
var reader = DirectoryReader.open(indexDir)
var vectors = reader.termVectors()
var storedFields = reader.storedFields()
Set projects = []
for (docId in 0..<reader.maxDoc()) {
String name = storedFields.document(docId).get('name')
TermsEnum terms = vectors.get(docId, 'content').iterator() // (1)
var found = [:]
while (terms.next() != null) {
PostingsEnum postingsEnum = terms.postings(null, PostingsEnum.ALL)
while (postingsEnum.nextDoc() != DocIdSetIterator.NO_MORE_DOCS) {
int freq = postingsEnum.freq()
var string = terms.term().utf8ToString().replaceAll('\n', ' ')
if (string.startsWith('apache ') || string.startsWith('eclipse ')) { // (2)
found[string] = freq
}
}
}
if (found) {
println "$name: $found"
projects += found.keySet()
}
}
var terms = projects.collect { name -> new Term('content', name) }
var byReverseValue = { e -> -e.value }
println "\nFrequency of total hits mentioning a project (top 10):"
var termFreq = terms.collectEntries { term ->
[term.text(), reader.totalTermFreq(term)] // (3)
}
display(termFreq.sort(byReverseValue).take(10), 50)
println "\nFrequency of documents mentioning a project (top 10):"
var docFreq = terms.collectEntries { term ->
[term.text(), reader.docFreq(term)] // (4)
}
display(docFreq.sort(byReverseValue).take(10), 20, 2)
-
获取所有索引词条
-
查找匹配项目名称的术语,这样我们就可以将它们保存到一个集合中。
-
获取我们词项集合中每个词项的命中频率元数据
-
获取我们词项集合中每个词项的文档频率元数据
当我们运行此代码时,我们看到
apache-nlpcraft-with-groovy.adoc: [apache nlpcraft:5] classifying-iris-flowers-with-deep.adoc: [apache commons math:1, apache spark:2, eclipse deeplearning4j:5] community-over-code-eu-2024.adoc: [apache beam:1, apache commons math:2, apache flink:1, apache ignite:1, apache ofbiz:1, apache spark:1, apache wayang:1] community-over-code-na-2023.adoc: [apache commons csv:1, apache commons numbers:1, apache ignite:8] deck-of-cards-with-groovy.adoc: [eclipse collections:5] deep-learning-and-eclipse-collections.adoc: [eclipse collections:7, eclipse deeplearning4j:2] detecting-objects-with-groovy-the.adoc: [apache mxnet:12] fruity-eclipse-collections.adoc: [apache commons math:1, eclipse collections:9] fun-with-obfuscated-groovy.adoc: [apache commons math:1] groovy-2-5-clibuilder-renewal.adoc: [apache commons cli:2] groovy-graph-databases.adoc: [apache age:11, apache hugegraph:3, apache tinkerpop:3] groovy-haiku-processing.adoc: [eclipse collections:3] groovy-list-processing-cheat-sheet.adoc: [apache commons collections:3, eclipse collections:4] groovy-lucene.adoc: [apache commons:4, apache commons math:2, apache lucene:3, apache nutch:1, apache solr:1, apache spark:1] groovy-null-processing.adoc: [apache commons collections:4, eclipse collections:6] groovy-pekko-gpars.adoc: [apache pekko:4] groovy-record-performance.adoc: [apache commons codec:1] handling-byte-order-mark-characters.adoc: [apache commons io:1] lego-bricks-with-groovy.adoc: [eclipse collections:6] matrix-calculations-with-groovy-apache.adoc: [apache commons:1, apache commons math:6, eclipse deeplearning4j:1] natural-language-processing-with-groovy.adoc: [apache opennlp:2, apache spark:1] reading-and-writing-csv-files.adoc: [apache commons csv:2] set-operations-with-groovy.adoc: [eclipse collections:3] solving-simple-optimization-problems-with-groovy.adoc: [apache commons math:4, apache kie:1] using-groovy-with-apache-wayang.adoc: [apache commons csv:1, apache flink:1, apache ignite:1, apache spark:7, apache wayang:9] whiskey-clustering-with-groovy-and.adoc: [apache commons csv:2, apache ignite:7, apache spark:2, apache wayang:1] wordle-checker.adoc: [eclipse collections:3] zipping-collections-with-groovy.adoc: [eclipse collections:4] Frequency of total hits mentioning a project (top 10): eclipse collections (50) ██████████████████████████████████████████████████▏ apache commons math (17) █████████████████▏ apache ignite (17) █████████████████▏ apache spark (14) ██████████████▏ apache mxnet (12) ████████████▏ apache wayang (11) ███████████▏ apache age (11) ███████████▏ eclipse deeplearning4j (8) ████████▏ apache commons collections (7) ███████▏ apache commons csv (6) ██████▏ Frequency of documents mentioning a project (top 10): eclipse collections (10) ████████████████████▏ apache commons math (7) ██████████████▏ apache spark (6) ██████████▏ apache ignite (4) ████████▏ apache commons csv (4) ████████▏ eclipse deeplearning4j (3) ██████▏ apache wayang (3) ██████▏ apache flink (2) ████▏ apache commons collections (2) ████▏ apache commons (2) ████▏
到目前为止,我们只展示了关于我们索引的精心策划的元数据。但为了表明我们有一个支持搜索的索引,让我们查找所有提到表情符号的文档。它们常常使编程示例变得非常有趣!
var parser = new QueryParser("content", analyzer)
var searcher = new IndexSearcher(reader)
var query = parser.parse('emoji*')
var results = searcher.search(query, 10)
println "\nTotal documents with hits for $query --> $results.totalHits"
results.scoreDocs.each {
var doc = storedFields.document(it.doc)
println "${doc.get('name')}"
}
当我们运行此代码时,我们看到
Total documents with hits for content:emoji* --> 11 hits adventures-with-groovyfx.adoc create-groovy-blog.adoc deep-learning-and-eclipse-collections.adoc fruity-eclipse-collections.adoc groovy-haiku-processing.adoc groovy-lucene.adoc helloworldemoji.adoc seasons-greetings-emoji.adoc set-operations-with-groovy.adoc solving-simple-optimization-problems-with-groovy.adoc
Lucene 拥有非常丰富的 API。现在我们来看看 Lucene 的一些替代用法。
我们通常会运行查询并探索这些结果,而不是探索索引元数据。我们现在将研究如何做到这一点。在探索查询结果时,我们将使用 `lucene-highlight` 模块中 `vectorhighlight` 包中的一些类。您通常会使用该模块中的功能来高亮显示命中,作为可能在网页上显示它们的一部分,作为某些网页搜索功能的一部分。对于我们来说,我们只会挑选出我们感兴趣的术语,即与我们的查询匹配的项目名称。
为了使高亮功能正常工作,我们要求索引器在索引时存储一些额外信息,特别是词项位置和偏移量。索引代码变为如下所示:
new IndexWriter(indexDir, config).withCloseable { writer ->
new File(baseDir).traverse(nameFilter: ~/.*\.adoc/) { file ->
file.withReader { br ->
var document = new Document()
var fieldType = new FieldType(stored: true,
indexOptions: IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS,
storeTermVectors: true,
storeTermVectorPositions: true,
storeTermVectorOffsets: true)
document.add(new Field('content', br.text, fieldType))
document.add(new StringField('name', file.name, Field.Store.YES))
writer.addDocument(document)
}
}
}
即使在之前的例子中,我们也可以存储这些额外信息,但之前并不需要。
接下来,我们定义一个辅助方法来从匹配项中提取实际的项目名称
List<String> handleHit(ScoreDoc hit, Query query, DirectoryReader dirReader) {
boolean phraseHighlight = true
boolean fieldMatch = true
var fieldQuery = new FieldQuery(query, dirReader, phraseHighlight, fieldMatch)
var stack = new FieldTermStack(dirReader, hit.doc, 'content', fieldQuery)
var phrases = new FieldPhraseList(stack, fieldQuery)
phrases.phraseList*.termsInfos*.text.flatten() // (1)
}
-
将 `FieldPhraseList` 转换为 `TermInfo` 实例列表,再转换为字符串列表。
定义了我们的辅助方法后,我们现在可以编写查询代码了。
var query = parser.parse(/apache\ * OR eclipse\ */) // (1)
var results = searcher.search(query, 30) // (2)
println "Total documents with hits for $query --> $results.totalHits\n"
var storedFields = searcher.storedFields()
var histogram = [:].withDefault { 0 }
results.scoreDocs.each { ScoreDoc scoreDoc -> // (3)
var doc = storedFields.document(scoreDoc.doc)
var found = handleHit(scoreDoc, query, reader) // (4)
println "${doc.get('name')}: ${found*.replaceAll('\n', ' ').countBy()}"
found.each { histogram[it.replaceAll('\n', ' ')] += 1 } // (5)
}
println "\nFrequency of total hits mentioning a project (top 10):"
display(histogram.sort { e -> -e.value }.take(10), 50) // (6)
-
搜索以 apache 或 eclipse 为前缀的术语
-
执行我们的查询,结果限制为 30 条
-
处理每个结果
-
提取实际匹配的术语
-
同时汇总计数
-
以漂亮的条形图形式显示前 10 个聚合结果
输出与之前基本相同
Total documents with hits for content:apache * content:eclipse * --> 28 hits classifying-iris-flowers-with-deep.adoc: [eclipse deeplearning4j:5, apache commons math:1, apache spark:2] fruity-eclipse-collections.adoc: [eclipse collections:9, apache commons math:1] groovy-list-processing-cheat-sheet.adoc: [eclipse collections:4, apache commons collections:3] groovy-null-processing.adoc: [eclipse collections:6, apache commons collections:4] matrix-calculations-with-groovy-apache.adoc: [apache commons math:6, eclipse deeplearning4j:1, apache commons:1] apache-nlpcraft-with-groovy.adoc: [apache nlpcraft:5] community-over-code-eu-2024.adoc: [apache ofbiz:1, apache commons math:2, apache ignite:1, apache spark:1, apache wayang:1, apache beam:1, apache flink:1] community-over-code-na-2023.adoc: [apache ignite:8, apache commons numbers:1, apache commons csv:1] deck-of-cards-with-groovy.adoc: [eclipse collections:5] deep-learning-and-eclipse-collections.adoc: [eclipse collections:7, eclipse deeplearning4j:2] detecting-objects-with-groovy-the.adoc: [apache mxnet:12] fun-with-obfuscated-groovy.adoc: [apache commons math:1] groovy-2-5-clibuilder-renewal.adoc: [apache commons cli:2] groovy-graph-databases.adoc: [apache age:11, apache hugegraph:3, apache tinkerpop:3] groovy-haiku-processing.adoc: [eclipse collections:3] groovy-lucene.adoc: [apache nutch:1, apache solr:1, apache lucene:3, apache commons:4, apache commons math:2, apache spark:1] groovy-pekko-gpars.adoc: [apache pekko:4] groovy-record-performance.adoc: [apache commons codec:1] handling-byte-order-mark-characters.adoc: [apache commons io:1] lego-bricks-with-groovy.adoc: [eclipse collections:6] natural-language-processing-with-groovy.adoc: [apache opennlp:2, apache spark:1] reading-and-writing-csv-files.adoc: [apache commons csv:2] set-operations-with-groovy.adoc: [eclipse collections:3] solving-simple-optimization-problems-with-groovy.adoc: [apache commons math:5, apache kie:1] using-groovy-with-apache-wayang.adoc: [apache wayang:9, apache spark:7, apache flink:1, apache commons csv:1, apache ignite:1] whiskey-clustering-with-groovy-and.adoc: [apache ignite:7, apache wayang:1, apache spark:2, apache commons csv:2] wordle-checker.adoc: [eclipse collections:3] zipping-collections-with-groovy.adoc: [eclipse collections:4] Frequency of total hits mentioning a project (top 10): eclipse collections (50) ██████████████████████████████████████████████████▏ apache commons math (18) ██████████████████▏ apache ignite (17) █████████████████▏ apache spark (14) █████████████▏ apache mxnet (12) ████████████▏ apache wayang (11) ███████████▏ apache age (11) ███████████▏ eclipse deeplearning4j (8) ████████▏ apache commons collections (7) ███████▏ apache commons csv (6) ██████▏
我们也可以聚合提到项目名称的文件计数。它看起来也会和以前一样。
使用 Lucene Facets
除了 Lucene 为自身目的在索引中存储的元数据之外,Lucene 还提供了一种机制,称为分面(facets),用于存储自定义元数据。分面允许进行更强大的搜索。它们通常用于将搜索结果分组到类别中。搜索用户可以深入到类别中以细化他们的搜索。
注意
|
分面是一个非常强大的功能。鉴于我们正在索引 asciidoc 源文件,我们甚至可以使用像 AsciidoctorJ 这样的库从我们的源文件中提取更多元数据并将其存储为分面。例如,我们可以提取标题、作者、关键词、发布日期等等。这将使我们能够进行一些非常强大的搜索。我们将其留作读者的练习。但是如果您尝试了,请告诉我们您的进展! |
我们用分面来存储每个文档的项目名称。一个捕获项目名称信息的分面可能就足够了,但为了说明 Lucene 的一些特性,我们将使用三个分面并在每个分面中存储略有不同的信息。
分面 | 类型 | 描述/示例 |
---|---|---|
|
|
项目引用命中计数,例如 “Apache Lucene” 在 `groovy-lucene.adoc` 中有 2 次命中。 |
|
|
按文档划分的项目引用,例如“Apache Spark”被 `classifying-iris-flowers-with-deep.adoc` 引用。 |
|
|
项目引用作为文档的层次结构,例如 ["Apache", "Commons", "Math"] 由 `fruity-eclipse-collections.adoc` 引用。 |
我们将使用正则表达式查找项目名称,并将信息存储在我们的分面中。Lucene 会创建一个特殊的**分类法(taxonomy)**索引来索引分面信息。我们也将启用它。
var analyzer = new ProjectNameAnalyzer()
var indexDir = new ByteBuffersDirectory()
var taxonDir = new ByteBuffersDirectory()
var config = new IndexWriterConfig(analyzer)
var indexWriter = new IndexWriter(indexDir, config) // (1)
var taxonWriter = new DirectoryTaxonomyWriter(taxonDir) // (2)
var fConfig = new FacetsConfig().tap { // (3)
setHierarchical('projectNameCounts', true)
setMultiValued('projectNameCounts', true)
setMultiValued('projectFileCounts', true)
setMultiValued('projectHitCounts', true)
setIndexFieldName('projectHitCounts', '$projectHitCounts')
}
new File(baseDir).traverse(nameFilter: ~/.*\.adoc/) { file ->
var m = file.text =~ tokenRegex
var projects = m*.get(2).grep()*.toLowerCase()*.replaceAll('\n', ' ').countBy()
file.withReader { br ->
var document = new Document()
var indexedWithFreq = new FieldType(stored: true,
indexOptions: IndexOptions.DOCS_AND_FREQS,
storeTermVectors: true)
document.add(new Field('content', br.text, indexedWithFreq))
document.add(new StringField('name', file.name, Field.Store.YES))
if (projects) {
println "$file.name: $projects"
projects.each { k, v -> // (4)
document.add(new IntAssociationFacetField(v, 'projectHitCounts', k))
document.add(new FacetField('projectFileCounts', k))
document.add(new FacetField('projectNameCounts', k.split()))
}
}
indexWriter.addDocument(fConfig.build(taxonWriter, document))
}
}
indexWriter.close()
taxonWriter.close()
-
我们的常规索引写入器
-
一个用于我们分类法的作者
-
定义我们感兴趣的分面的一些属性
-
我们将感兴趣的分面添加到文档中
既然我们正在索引时收集项目名称,我们就可以将它们打印出来
apache-nlpcraft-with-groovy.adoc: [apache nlpcraft:5] classifying-iris-flowers-with-deep.adoc: [eclipse deeplearning4j:5, apache commons math:1, apache spark:2] community-over-code-eu-2024.adoc: [apache ofbiz:1, apache commons math:2, apache ignite:1, apache spark:1, apache wayang:1, apache beam:1, apache flink:1] community-over-code-na-2023.adoc: [apache ignite:8, apache commons numbers:1, apache commons csv:1] deck-of-cards-with-groovy.adoc: [eclipse collections:5] deep-learning-and-eclipse-collections.adoc: [eclipse collections:7, eclipse deeplearning4j:2] detecting-objects-with-groovy-the.adoc: [apache mxnet:12] fruity-eclipse-collections.adoc: [eclipse collections:9, apache commons math:1] fun-with-obfuscated-groovy.adoc: [apache commons math:1] groovy-2-5-clibuilder-renewal.adoc: [apache commons cli:2] groovy-graph-databases.adoc: [apache age:11, apache hugegraph:3, apache tinkerpop:3] groovy-haiku-processing.adoc: [eclipse collections:3] groovy-list-processing-cheat-sheet.adoc: [eclipse collections:4, apache commons collections:3] groovy-lucene.adoc: [apache nutch:1, apache solr:1, apache lucene:3, apache commons:4, apache commons math:2, apache spark:1] groovy-null-processing.adoc: [eclipse collections:6, apache commons collections:4] groovy-pekko-gpars.adoc: [apache pekko:4] groovy-record-performance.adoc: [apache commons codec:1] handling-byte-order-mark-characters.adoc: [apache commons io:1] lego-bricks-with-groovy.adoc: [eclipse collections:6] matrix-calculations-with-groovy-apache.adoc: [apache commons math:6, eclipse deeplearning4j:1, apache commons:1] natural-language-processing-with-groovy.adoc: [apache opennlp:2, apache spark:1] reading-and-writing-csv-files.adoc: [apache commons csv:2] set-operations-with-groovy.adoc: [eclipse collections:3] solving-simple-optimization-problems-with-groovy.adoc: [apache commons math:5, apache kie:1] using-groovy-with-apache-wayang.adoc: [apache wayang:9, apache spark:7, apache flink:1, apache commons csv:1, apache ignite:1] whiskey-clustering-with-groovy-and.adoc: [apache ignite:7, apache wayang:1, apache spark:2, apache commons csv:2] wordle-checker.adoc: [eclipse collections:3] zipping-collections-with-groovy.adoc: [eclipse collections:4]
现在进行搜索时,我们可以提取分类信息以及其他信息。通过 `projectHitCounts`,我们可以从搜索结果中获取最高命中的分类元数据。我们将使用 `MatchAllDocsQuery` 来匹配所有文档,即元数据将适用于所有文档。
var reader = DirectoryReader.open(indexDir)
var searcher = new IndexSearcher(reader)
var taxonReader = new DirectoryTaxonomyReader(taxonDir)
var fcm = new FacetsCollectorManager()
var query = new MatchAllDocsQuery()
var fc = FacetsCollectorManager.search(searcher, query, 0, fcm).facetsCollector()
var topN = 5
var projects = new TaxonomyFacetIntAssociations('$projectHitCounts', taxonReader,
fConfig, fc, AssociationAggregationFunction.SUM)
var hitData = projects.getTopChildren(topN, 'projectHitCounts').labelValues
println "\nFrequency of total hits mentioning a project (top $topN):"
display(hitData.collectEntries{ lv -> [lv.label, lv.value] }, 50)
println "\nFrequency of documents mentioning a project (top $topN):"
display(hitData.collectEntries{ lv -> [lv.label, lv.count] }, 20, 2)
当运行此代码时,我们可以看到总命中数和文件数的频率
Frequency of total hits mentioning a project (top 5): eclipse collections (50) ██████████████████████████████████████████████████▏ apache commons math (18) ██████████████████▏ apache ignite (17) █████████████████▏ apache spark (14) ██████████████▏ apache mxnet (12) ████████████▏ Frequency of documents mentioning a project (top 5): eclipse collections (10) ████████████████████▏ apache commons math (7) ██████████████▏ apache ignite (4) ████████▏ apache spark (6) ████████████▏ apache mxnet (1) ██▏
现在,关于文档频率的分类法信息是基于命中数得分最高的命中。我们的另一个分面(`projectFileCounts`)独立跟踪文档频率。让我们看看如何查询该信息。
var facets = new FastTaxonomyFacetCounts(taxonReader, fConfig, fc)
println "\nFrequency of documents mentioning a project (top $topN):"
println facets.getTopChildren(topN, 'projectFileCounts')
我们可以像之前一样将搜索结果(`FacetResult` 实例)显示为条形图,但结果的 `toString` 也非常有用。以下是运行上述代码的样子:
Frequency of documents mentioning a project (top 5): dim=projectFileCounts path=[] value=-1 childCount=27 eclipse collections (10) apache commons math (7) apache spark (6) apache ignite (4) apache commons csv (4)
将此结果与我们上一个分面的结果进行比较时,我们可以看到 commons csv 在更多文件中被提及,尽管 mxnet 被提及的次数更多。一般来说,您会决定哪个文档频率对您更感兴趣,如果您不需要这些额外信息,则会跳过 `projectFileCounts` 分面。
我们的最后一个分面(`projectNameCounts`)是一个分层分面。这些通常在“浏览”搜索结果时交互式使用。我们可以按第一个词查看项目名称,例如基金会。然后我们可以深入到其中一个基金会,例如“Apache”,并找到引用的项目,然后对于 commons,我们可以深入到其子项目。以下是执行此操作的代码。
['apache', 'commons'].inits().reverseEach { path -> // (1)
println "Frequency of documents mentioning a project with path $path (top $topN):"
println "${facets.getTopChildren(topN, 'projectNameCounts', *path)}"
}
-
`inits()` 方法返回一个列表的所有前缀,包括空列表。
输出如下
Frequency of documents mentioning a project with path [] (top 5): dim=projectNameCounts path=[] value=-1 childCount=2 apache (21) eclipse (12) Frequency of documents mentioning a project with path [apache] (top 5): dim=projectNameCounts path=[apache] value=-1 childCount=18 commons (16) spark (6) ignite (4) wayang (3) flink (2) Frequency of documents mentioning a project with path [apache, commons] (top 5): dim=projectNameCounts path=[apache, commons] value=-1 childCount=7 math (7) csv (4) collections (2) numbers (1) cli (1)
我们现在既有分类法索引,也有普通索引,所以我们仍然可以进行可能只使用后者的即席查询。
var parser = new QueryParser('content', analyzer)
var query = parser.parse(/apache\ * AND eclipse\ * AND emoji*/)
var results = searcher.search(query, topN)
var storedFields = searcher.storedFields()
assert results.totalHits.value() == 1 &&
storedFields.document(results.scoreDocs[0].doc).get('name') == 'fruity-eclipse-collections.adoc'
此查询显示,恰好有一篇博客文章提到了 Apache 项目、Eclipse 项目以及表情符号。
更复杂的查询
作为最后一个例子,我们之前选择在索引时提取项目名称。我们也可以改用更典型的分析器,代价是需要在搜索时使用更复杂的跨度查询来提取项目名称。让我们看看这种情况下代码可能是什么样子。
首先,我们将使用 `StandardAnalyzer` 进行索引。
var analyzer = new StandardAnalyzer()
var indexDir = new ByteBuffersDirectory()
var config = new IndexWriterConfig(analyzer)
new IndexWriter(indexDir, config).withCloseable { writer ->
new File(baseDir).traverse(nameFilter: ~/.*\.adoc/) { file ->
file.withReader { br ->
var document = new Document()
var fieldType = new FieldType(stored: true,
indexOptions: IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS,
storeTermVectors: true,
storeTermVectorPositions: true,
storeTermVectorOffsets: true)
document.add(new Field('content', br.text, fieldType))
document.add(new StringField('name', file.name, Field.Store.YES))
writer.addDocument(document)
}
}
}
现在我们的查询需要更复杂。我们有几种选择,但我们将选择使用 Lucene 的一些低级查询类来组合我们的查询。
注意
|
在考虑 Lucene 的低级查询类之前,您可能需要查看 Lucene 的一些高级查询类,例如 `QueryParser` 类。它支持将查询表示为字符串,并包含对短语、范围、正则表达式术语等的支持。据我所知,它不支持短语中的正则表达式,因此我们将探索下面的低级类。 |
我们将查找类似“apache commons
我们不会像正则表达式那样有一个停用词(排除词)列表,而是会有一个允许的项目后缀名称列表。如果需要,切换到停用词方法也很容易。
IndexReader reader = DirectoryReader.open(indexDir)
var searcher = new IndexSearcher(reader)
var projects = [
'math', 'spark', 'lucene', 'collections', 'deeplearning4j',
'beam', 'wayang', 'csv', 'io', 'numbers', 'ignite', 'mxnet', 'age',
'nlpcraft', 'pekko', 'hugegraph', 'tinkerpop', 'commons',
'cli', 'opennlp', 'ofbiz', 'codec', 'kie', 'flink'
]
var suffix = new SpanMultiTermQueryWrapper(new RegexpQuery( // (1)
new Term('content', "(${projects.join('|')})")))
// look for apache commons <suffix>
SpanQuery[] spanTerms = ['apache', 'commons'].collect{
new SpanTermQuery(new Term('content', it))
} + suffix
var apacheCommons = new SpanNearQuery(spanTerms, 0, true)
// look for (apache|eclipse) <suffix>
var foundation = new SpanMultiTermQueryWrapper(new RegexpQuery(
new Term('content', '(apache|eclipse)')))
var otherProject = new SpanNearQuery([foundation, suffix] as SpanQuery[], 0, true)
var builder = new BooleanQuery.Builder(minimumNumberShouldMatch: 1)
builder.add(otherProject, BooleanClause.Occur.SHOULD)
builder.add(apacheCommons, BooleanClause.Occur.SHOULD)
var query = builder.build()
var results = searcher.search(query, 30)
println "Total documents with hits for $query --> $results.totalHits"
-
正则表达式查询被包装以出现在跨度查询中
当我们运行这个时,我们看到的命中数与之前相同。
Total documents with hits for (spanNear([SpanMultiTermQueryWrapper(content:/(apache|eclipse)/), SpanMultiTermQueryWrapper(content:/(math|spark|lucene|collections|deeplearning4j|beam|wayang|csv|io|numbers|ignite|mxnet|age|nlpcraft|pekko|hugegraph|tinkerpop|commons|cli|opennlp|ofbiz|codec|kie|flink)/)], 0, true) spanNear([content:apache, content:commons, SpanMultiTermQueryWrapper(content:/(math|spark|lucene|collections|deeplearning4j|beam|wayang|csv|io|numbers|ignite|mxnet|age|nlpcraft|pekko|hugegraph|tinkerpop|commons|cli|opennlp|ofbiz|codec|kie|flink)/)], 0, true))~1 --> 28 hits
对于这个例子,我们可能要考虑的另一件事是利用 Groovy 优秀的领域特定语言(DSL)功能。通过定义一个辅助方法 `span`,并为 Lucene 的 `Query` 类提供一个 `or` 的元编程扩展,我们可以用更紧凑和易懂的形式重写上一个例子的最后 20 行:
var suffix = "(${projects.join('|')})"
var query = span('apache', 'commons', ~suffix) | span(~'(apache|eclipse)', ~suffix)
var results = searcher.search(query, 30)
println "Total documents with hits for $query --> $results.totalHits"
运行代码会得到与之前相同的输出。如果您对 DSL 细节感兴趣,请查看源文件。
我们可以在其他词项上尝试我们的 DSL
query = span('jackson', 'databind') | span(~'virt.*', 'threads')
results = searcher.search(query, 30)
println "Total documents with hits for $query --> $results.totalHits"
运行时,我们将看到如下输出:
Total documents with hits for (spanNear([content:jackson, content:databind], 0, true) spanNear([SpanMultiTermQueryWrapper(content:/virt.*/), content:threads], 0, true))~1 --> 8 hits
使用 `StandardAnalyzer` 配合跨度查询无疑开辟了更广泛的查询可能性。但 `StandardAnalyzer` 也有其他优点。它内置了停用词、智能分词、小写转换等功能。其他内置分析器也可能有用。当然,我们也可以让我们的基于正则表达式的分析器更智能。Lucene 的许多功能都是可重用部件,这确实很有帮助。
`StandardAnalyzer` 的一个有趣优势是它能正确处理索引中的表情符号。我们的正则表达式分析器目前只查找“正则表达式单词”字符,不包括表情符号字符,尽管它可以扩展以支持它们。
鉴于我们在此使用了 `StandardAnalyzer`,让我们再次查看索引中的术语,但这次提取表情符号而不是项目名称。
var vectors = reader.termVectors()
var storedFields = reader.storedFields()
var emojis = [:].withDefault { [] as Set }
for (docId in 0..<reader.maxDoc()) {
String name = storedFields.document(docId).get('name')
TermsEnum terms = vectors.get(docId, 'content').iterator()
while (terms.next() != null) {
PostingsEnum postingsEnum = terms.postings(null, PostingsEnum.ALL)
while (postingsEnum.nextDoc() != DocIdSetIterator.NO_MORE_DOCS) {
var string = terms.term().utf8ToString()
if (string.codePoints().allMatch(Character::isEmojiPresentation)) {
emojis[name] += string
}
}
}
}
emojis.collect { k, v -> "$k: ${v.join(', ')}" }.each { println it }
运行时,您应该会看到类似这样的内容(部分平台可能无法显示旗帜表情符号)
结论
我们已经分析了 Groovy 博客文章,使用正则表达式和 Apache Lucene 查找引用的项目。希望这能让您领略 Lucene API 和 Groovy 的一些功能。