使用索引数据计算非结构化文档中的所有唯一单词

时间:2014-08-20 11:10:46

标签: marklogic

我已将非结构化HTML文档加载到Marklogic中, 对于任何给定的文档URI,我需要一种方法来使用索引/词典来为所有唯一单词提供单词计数。

例如,假设我有以下文件,保存在URI“/html/example.html”下:

<html>
<head><title>EXAMPLE</title></head>
<body>
<h1>This is a header</h1>
<div class="highlight">This word is highlighted</div>
<p> And these words are inside a paragraph tag</p>
</body>
</html>

在XQuery中,我通过传递URI来调用我的函数传递,并得到以下结果:

EXAMPLE 1
This 2
is 2
a 2
header 1
word 1
highlighted 1
And 1
these 1
words 1
are 1
inside 1
paragraph 1
tag 1

请注意,我只需要对标签内的单词进行单词计数,而不是标签本身。

有没有办法有效地做到这一点(使用索引或词典数据?)

谢谢,

grifster

2 个答案:

答案 0 :(得分:2)

您要求任何给定文档URI&#34;的字数和#34;。但是你假设解决方案涉及索引或词典,并且这不一定是一个好的假设。如果您希望从面向文档的数据库中获取特定于文档的内容,则通常最好直接处理该文档。

因此,让我们专注于单个文档的高效字数统计解决方案,并从那里开始。 OK?

以下是我们如何为单个元素(包括任何子元素)获取字数。这可能是您的文档的根目录:doc($uri)/*

declare function local:word-count($root as element())
as map:map
{
  let $m := map:map()
  let $_ := cts:tokenize(
    $root//text())[. instance of cts:word]
    ! map:put($m, ., 1 + (map:get($m, .), 0)[1])
  return $m
};

这会产生一张地图,我发现它比平面文字更灵活。每个键都是一个单词,值是计数。变量$doc已包含您的示例XML。

let $m := local:word-count($doc)
for $k in map:keys($m)
return text { $k, map:get($m, $k) }

inside 1
This 2
is 2
paragraph 1
highlighted 1
EXAMPLE 1
header 1
are 1
word 1
words 1
these 1
tag 1
And 1
a 2

请注意,地图键的顺序是不确定的。如果您愿意,可以添加order by子句。

let $m := local:word-count($doc)
for $k in map:keys($m)
let $v := map:get($m, $k)
order by $v descending
return text { $k, $v }

如果您想查询整个数据库,使用cts:words的Geert解决方案可能看起来不错。它使用单词列表的词典,以及单词匹配的一些索引查找。但是对于每个单词词典单词:O(nm),它最终会为每个匹配的文档运行XML。要做到这一点,代码必须做的工作类似local:word-count所做的工作,但一次一个字。许多单词将匹配相同的文档:&#39;&#39;可能在A和B中,然后&#39;然后&#39;可能也在A和B中。尽管使用词典和索引,通常这种方法比简单地将local:word-count应用于整个数据库要慢。

如果要查询整个数据库并愿意更改XML,可以将每个单词包装在word元素(或您喜欢的任何元素名称)中。然后在word上创建一个string类型的元素范围索引。现在,您可以使用cts:valuescts:frequency直接从范围索引中提取答案。这将是O(n),其成本远低于cts:words方法,并且可能比local:word-count更快,因为根本不会访问任何文档。但是生成的XML非常笨拙。

让我们返回并将local:word-count应用于整个数据库。首先调整代码,以便调用者提供地图。这样我们就可以构建一个具有整个数据库字数的单一地图,我们只查看每个文档一次。

declare function local:word-count(
  $m as map:map,
  $root as element())
as map:map
{
  let $_ := cts:tokenize(
    $root//text())[. instance of cts:word]
    ! map:put($m, ., 1 + (map:get($m, .), 0)[1])
  return $m
};

let $m := map:map()
let $_ := local:word-count($m, collection()/*)
for $k in map:keys($m)
let $v := map:get($m, $k)
order by $v descending
return text { $k, $v }

在我的笔记本电脑上,它在不到100毫秒的时间内处理了151个文档。大约有8100个单词和925个不同的单词。从cts:wordscts:search获得相同的结果只需不到1秒。所以local:word-count效率更高,而且可能足以完成这项工作。

既然您可以有效地构建字数统计地图,那么如果可以保存它呢?从本质上讲,您构建了我们自己的&#34;索引&#34;字数。这很简单,因为地图具有XML序列化。

(: Construct a map. :)
map:map()
(: The document constructor creates a document-node with XML inside. :)
! document { . }
(: Construct a map from the XML root element. :)
! map:map(*)

因此,您可以在插入或更新每个新XML文档时调用local:word-count。然后将字数映射存储在文档的属性中。使用CPF管道,或通过RecordLoader或REST上传端点等使用您自己的代码来执行此操作。

当您想要单个文档的字数时,只需调用xdmp:document-propertiesxdmp:document-get-properties,然后调用右侧XML上的map:map构造函数。如果您想要多个文档的字数,您可以轻松编写XQuery以将这些地图合并为一个结果。

答案 1 :(得分:0)

您通常会将cts:frequency用于此目的。不幸的是,这只能提供给从词汇词汇中提取的值,而不能提供给词汇词典中的值。因此,我担心您必须手动计数,除非您可以将所有单词预先标记为可以放置范围索引的元素。我能想到的最接近的一件事是:

for $word in cts:words()
let $freq := count(cts:search(doc()//*,$word))
order by $freq descending
return concat($word, ' - ', $freq)

注意:doc()将搜索所有文档,因此严重缩放。但如果您对每个文档的计数感兴趣,那么性能可能对您来说足够好......