我想在没有任何API的情况下构建一个简单的搜索引擎索引功能,例如Lucene。在倒排索引中,我只需要记录每个单词的基本信息,例如docID,position和freqence。
现在,我有几个问题:
通常使用什么样的数据结构来构建倒排索引?多维列表?
构建索引后,如何将其写入文件?文件中有哪种格式?像一张桌子?就像在纸上画一个索引表一样?
答案 0 :(得分:31)
您可以在TinySearchEngine中看到倒置索引和搜索的非常简单的实现。
对于你的第一个问题,如果你想构建一个简单的(内存中)倒排索引,那么简单的数据结构就是这样的Hash映射:
val invertedIndex = new collection.mutable.HashMap[String, List[Posting]]
或Java-esque:
HashMap<String, List<Posting>> invertedIndex = new HashMap<String, List<Postring>>();
哈希将每个术语/单词/标记映射到过帐列表。 Posting
只是一个对象,表示文档中出现的单词:
case class Posting(docId:Int, var termFrequency:Int)
索引新文档只需将其标记(用标记/单词分隔),并为每个标记在哈希映射的正确列表中插入新的过帐。当然,如果该特定docId中的该术语已存在,则增加termFrequency。还有其他方法可以做到这一点。对于内存中的反向索引,这没关系,但对于磁盘索引,您可能希望使用正确的Postings
插入termFrequency
一次,而不是每次都更新它。
关于你的第二个问题,通常有两种情况:
(1)你有一个(几乎)不可变的索引。您可以将所有数据编入索引一次,如果有新数据,则可以重新编制索引。例如,无需在一小时内多次实时或索引。
(2)新文件一直到达,您需要尽快搜索新到的文件。
对于案例(1),您可以拥有至少2个文件:
1 - 倒置索引文件。它为每个术语列出所有Postings
(docId / termFrequency对)。这里用纯文本表示,但通常存储为二进制数据。
Term1<docId1,termFreq><docId2,termFreq><docId3,termFreq><docId4,termFreq><docId5,termFreq><docId6,termFreq><docId7,termFreq>
Term2<docId3,termFreq><docId5,termFreq><docId9,termFreq><docId10,termFreq><docId11,termFreq>
Term3<docId1,termFreq><docId3,termFreq><docId10,termFreq>
Term4<docId5,termFreq><docId7,termFreq><docId10,termFreq><docId12,termFreq>
...
TermN<docId5,termFreq><docId7,termFreq>
2-偏移文件。存储每个术语的偏移量,以在倒排索引文件中查找其反转列表。这里我用字符表示偏移量但你通常会存储二进制数据,因此偏移量将以字节为单位。此文件可以在启动时加载到内存。当您需要查找术语反转列表时,可以查找其偏移量并从文件中读取反转列表。
Term1 -> 0
Term2 -> 126
Term3 -> 222
....
除了这2个文件,你可以(通常会)有文件来存储每个术语的IDF和每个文档的标准。
对于案例(2),我将尝试简要解释Lucene(以及Solr和ElasticSearch)如何做到这一点。
文件格式可以与上面说明的相同。主要区别在于,在Lucene等系统中索引新文档而不是从头开始重建索引时,只需创建一个只包含新文档的新文档。因此,每次必须索引某些内容时,都要在新的分离索引中进行索引。
要在此“拆分”索引中执行查询,您可以针对每个不同的索引(并行)运行查询,并在返回给用户之前将结果合并在一起。
Lucene将这个“小”索引称为segments
。
这里显而易见的问题是,你会很快得到很多小段。为避免这种情况,您需要一个合并细分和创建更大细分的策略。例如,如果您的N segments
超过10 KBs
,则可以决定合并所有小于{{1}}的细分。