优化反向索引Java

时间:2015-10-10 11:39:19

标签: java optimization garbage-collection

我正在尝试为维基百科页面创建一个倒排索引但是我的内存不足。我不知道还能做些什么来确保它不会耗尽内存。但是我们谈的是3.9Mil的话。

indexer.java

public void index() {
    ArrayList<Page> pages = parse(); // Parse XML pages
    HashMap<String, ArrayList<Integer>> postings = getPostings(pages);
}

public HashMap<String, ArrayList<Integer>> getPostings(ArrayList<Page> pages) {
    assert pages != null;

    englishStemmer stemmer = new englishStemmer();
    HashSet<String> stopWords = getStopWords();
    HashMap<String, ArrayList<Integer>> postings = new HashMap<>();
    int count = 0;
    int artCount = 0;

    for (Page page : pages) {

        if (!page.isRedirect()) { // Skip pages that are redirects.

            StringBuilder sb = new StringBuilder();
            artCount = count; // All the words until now
            boolean ignore = false;

            for (char c : page.getText().toCharArray()) {

                if (c == '<') // Ignore words inside <> tags.
                    ignore = true;

                if (!ignore) {
                    if (c != 39) {
                        if (c > 47 && c < 58 || c > 96 && c < 123) // Character c is a number 0-9 or a lower case letter a-z.
                            sb.append(c);

                        else if (c > 64 && c < 91) // Character c is an uppercase letter A-Z.
                            sb.append(Character.toLowerCase(c));

                        else if (sb.length() > 0) { // Check if there is a word up until now.

                            if (sb.length() > 1) { // Ignore single character "words"

                                if (!stopWords.contains(sb.toString())) { // Check if the word is not a stop word.

                                    stemmer.setCurrent(sb.toString());
                                    stemmer.stem(); // Stem word s

                                    String s = sb.toString(); // Retrieve the stemmed word

                                    if (!postings.containsKey(s)) // Check if the word already exists in the words map.
                                        postings.put(s, new ArrayList<>()); // If the word is not in the map then create an array list for that word.
                                    postings.get(s).add(page.getId()); // Place the id of the page in the word array list.
                                    count++; // Increase the overall word count for the pages
                                }
                            }
                            sb = new StringBuilder();
                        }
                    }
                }

                if (c == '>')
                    ignore = false;
            }
        }
        page.setCount(count - artCount);
    }
    System.out.println("Word count:" + count);
    return postings;
}

优点

这种方法的一些优点是:

  • 只需获取相关ArrayList的大小即可获得给定单词的出现次数。
  • 查看页面中出现给定单词的次数相对容易。

优化

目前的优化:

  • 忽略常用词(停用词)。
  • 根据他们的根源并存储这些词语。
  • 忽略不是英语单词的常见维基百科标签(包含在停用词列表中,例如:lt,gt,ref ..等)。
  • 忽略< >代码中的文字,例如:<pre>, <div>

限制

数组列表变得非常大,单词的出现次数,这种方法的主要缺点是当数组列表必须增长时。将创建一个新的数组列表,并且需要将先前数组列表中的项目复制到新数组列表中。这可能是一个可能的性能瓶颈。链接列表会更有意义吗?因为我们只是添加更多事件而不是读取事件。这也意味着由于链接列表不依赖于数组作为其底层数据结构,因此它们可以无限制地增长,并且当它们太大时不需要替换。

替代方法

我已经考虑在处理完每个页面之后将每个单词的计数转储到像MongoDB这样的数据库中,然后追加新的出现次数。它将是:{word : [occurrences]},然后在每个页面处理完毕后让GC清除postings HashMap。

我还考虑将页面循环移动到index()方法,以便GC可以在新页面之前清除getPostings()。然后在每页之后合并新postings,但我认为这不会减轻内存负担。

至于哈希映射,树图会更适合这种情况吗?

执行

在我的机器上,这个程序在所有4个内核上运行,使用90-100%,并占用大约2-2.5GB RAM。它运行超过一个半小时:GC Out of memory

我还考虑过增加这个程序的可用内存,但它也需要在我的教师机器上运行。所以它需要作为标准操作,没有任何&#34; hacks&#34;。

我需要帮助做出相当大的优化,我不确定还有什么可以帮助。

2 个答案:

答案 0 :(得分:2)

TL; DR 无论你做什么,很可能你的数据结构都不适合内存。

旁注:您应该实际解释您的任务是什么以及您的方法是什么。你不这样做,希望我们阅读你的代码。

你基本上在做的是建立一个单词的多图 - &gt;维基百科文章的ID。为此,您解析每个非重定向页面,将其划分为单个单词,并通过添加单词构建多图 - &gt;页面ID映射。

让我们粗略估计一下这个结构有多大。你的假设是大约400万字。 EN维基百科中有大约500万篇文章。英语的平均字长约为5个字符,因此我们假设每个字10个字节,每个文章ID 4个字节。我们的单词大约为40 MB(地图中的键),文章ID为20 MB(地图中的值)。
假设类似multihashmap的结构,您可以估计大小为32 * size + 4 *容量的hashmap大小。

到目前为止,这似乎是可以管理的,几十MB。

但是将会有大约4百万个集合来存储文章ID,每个集合大小约为8 *(如果你将采用数组列表),其中大小是一个单词将会遇到的一些文章。到http://www.wordfrequency.info/,COCAE中提到的前5000个词超过3亿次,所以我希望维基百科能够在这个范围内。
对于仅5k顶级单词的文章ID,这大约是2.5 GB。这是一个很好的暗示,你的倒排索引结构可能会占用太多内存,无法放在一台机器上。

但是我不认为你对结果结构的大小有问题。您的代码表示您首先在内存中加载页面并稍后处理它们。这绝对不会奏效。

您很可能需要以类似流的方式处理页面并使用某种数据库来存储结果。基本上有一千种方法可以做到这一点,我个人使用PostgreSQL作为数据库在AWS上使用Hadoop作业,利用UPSERT功能。

答案 1 :(得分:1)

ArrayList是您必须编写的类索引替换的候选者。它应该使用int []存储索引值和重新分配策略,该策略使用基于其所属单词的总体增长率的增量。 (ArrayList增加旧值的50%,对于罕见的单词,这可能不是最佳的。)此外,它应该通过存储第一个索引和后续数字的负数来为优化范围的存储留出空间,例如,< / p>

..., 100, -3,...   is index values for 100, 101, 102, 103

这可能导致以几个周期为代价保存频繁出现的单词的条目。

在输入一定数量的索引值和带有空映射的续集后,考虑转储HashMap的转储。如果文件按键排序,则允许相对简单地合并两个或多个文件。