在超过 600,000 个文件中进行快速文本搜索

Fast text search in over 600,000 files

我有一个 php、linux 服务器。它有一个名为 notes_docs 的文件夹,其中包含超过 600,000 个 txt 文件。 notes_docs的文件夹结构如下-

 - notes_docs
   - files_txt
     - 20170831
           - 1_837837472_abc_file.txt
           - 1_579374743_abc2_file.txt
           - 1_291838733_uridjdh.txt
           - 1_482737439_a8weele.txt
           - 1_733839474_dejsde.txt
     - 20170830
     - 20170829

我必须提供一个可以在浏览器上显示结果的快速文本搜索实用程序。因此,如果我的用户搜索 "new york",则所有包含 "new york" 的文件都应在数组中返回。如果用户搜索 "foo",则应返回所有包含 "foo" 的文件。

我已经尝试过使用 scandirDirectory Iterator 的代码,这太慢了。搜索需要一分多钟,即使这样搜索也没有完成。我尝试了 ubuntu find ,它再次缓慢地花了一分钟才能完成。因为文件夹迭代太多,notes_docs当前大小超过 20 GB。

欢迎任何我可以用来加快速度的解决方案。我可以进行设计更改,集成我的 PHP 代码以 curl 到另一种语言代码。在极端情况下,我也可以更改基础设施(例如在内存中使用某些东西)。

我想知道业内人士是怎么做到的? Indeed、Zip Recruiter 的人都提供文件搜索。

请注意我有 2GB - 4GB 的 RAM,所以一直将所有文件加载到 RAM 上是不可接受的。

编辑 - 以下所有输入都很棒。对于后来的人,我们最终使用 Lucene 进行索引和文本搜索。表现真的很好

为简单起见:没有快速打开、搜索和关闭 600k 文档的方法。您使用 "over a minute" 的基准测试可能是针对单个测试帐户的。如果您打算通过多用户网站搜索这些内容,您很快就会忘记它,因为您的 disk IO 将超出排行榜并阻止您的整个服务器。

所以您唯一的选择是索引所有文件。就像其他所有快速搜索实用程序一样。无论您是使用评论中提到的 Solr 还是 ElasticSearch,还是构建您自己的东西。文件将被编入索引。

考虑到 txt 文件是您收到的 pdf 文件的文本版本,我敢打赌最简单的解决方案是将文本写入数据库而不是文件。它不会占用更多的磁盘 space 无论如何。

然后您可以在您的数据库上启用 full text searchmysqlmssql 和其他支持它),我相信响应时间会好很多。请记住,创建这些 indexes 确实需要存储 space,但其他解决方案也是如此。

现在如果你真的想加快速度,你可以尝试在更详细的层面上解析简历。尝试检索您经常搜索的位置、教育、口语和其他信息,并将它们放在单独的 tables/columns 中。这是一项非常艰巨的任务,几乎是一个独立的项目,但如果您想要一个有价值的搜索结果,这就是您要走的路。因为在没有上下文的情况下搜索文本会产生截然不同的结果,所以想想你的例子 "new york":

  1. 我住在纽约
  2. 我就读于纽约大学
  3. 我喜欢 Alicia Keys 在个人传记中的歌曲 "new york"
  4. 我在纽约披萨店工作
  5. 我出生在英国纽约州
  6. 我花了一个夏天饲养纽约夏犬。

我不会讲得太深,但我会尝试提供创建概念验证的指南。

1

首先从这里下载并提取弹性搜索:https://www.elastic.co/downloads/elasticsearch 然后 运行 它:

bin/elasticsearch

2

下载https://github.com/dadoonet/fscrawler#download-fscrawler解压并运行:

bin/fscrawler myCustomJob

然后停止它(Ctrl-C)并编辑相应的myCustomJob/_settings.json(它已经自动创建并且路径打印在控制台上)。
您可以编辑属性:"url"(要扫描的路径), "update_rate"(你可以做到1m), "includes"(例如 ["*.pdf","*.doc","*.txt"]),"index_content"(设为假,只保留在文件名上)。

运行 再一次:

bin/fscrawler myCustomJob

注意:索引是您稍后可能希望使用代码执行的操作,但目前,它会自动完成,使用 fscrawler,直接与 elastic 对话。

3

现在开始将文件添加到您在 "url" 属性 中指定的目录中。

4

为 chrome 下载 advanced rest client 并进行以下 POST:

URL: http://localhost:9200/_search

原始负载:

{
  "query": { "wildcard": {"file.filename":"aFileNameToSearchFor*"} }
}

您将得到匹配文件的列表。注意:fscrawler 正在索引键下的文件名:file.filename.

5

现在,您可以使用 PHP 而不是使用 高级休息客户端 来执行此查询。通过对上述 url 的 REST 调用,或利用 php-客户端 api:https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/_search_operations.html

同样代表索引:https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/_indexing_documents.html

如果您想将所有文件信息保存到数据库中:

<?php 
function deep_scandir( $dir, &$query, &$files) {
    
    $count = 0;
    
    if(is_dir($dir)) {
        if ($dh = opendir($dir)) {
            while (($item = readdir($dh)) !== false) {
                if($item != '.' && $item != '..') {
                    if(is_dir($dir.'/'.$item)){
                        deep_scandir($dir.'/'.$item, $query, $files);
                    }else{
                        $count++;
                        preg_match("/(\d\_\d+)\_(.*)\.txt/i", $item, $matches);
                        if(!empty($matches)){
                            $no = $matches[1];
                            $str = $matches[2];
                            $files[$dir][$no] = $str;
                            $content = addcslashes( htmlspecialchars( file_get_contents($dir.'/'.$item) ), "\\'\"" );
                            $query[] =  "INSERT INTO `mytable` (id, key, value, path, content)
                            VALUES\n(NULL, '$no', '$str', '$dir/$item', '$content');";
                        }
                    }
                }
            }
            closedir($dh);
        }
    }
    return $count;
}
    
echo '<pre>';
$dir = 'notes_docs/files_txt';
$query = [];
$files = [];
echo deep_scandir($dir, $query, $files);
echo '<br>';
print_r($files);
echo '<br>';
print_r($query);

现在你可以执行数组中的每一行

foreach($query as $no=>$line){
    mysql_query($line) or trigger_error("Couldn't execute query no: '$no' [$line]");
}

输出:

Array
(
    [notes_docs/files_txt/20170831] => Array
        (
            [1_291838733] => uridjdh
            [1_482737439] => a8weele
            [1_579374743] => abc2_file
            [1_733839474] => dejsde
            [1_837837472] => abc_file
        )

)

Array
(
    [0] => INSERT INTO `mytable` (id, key, value, path, content)
                            VALUES
(NULL, '1_291838733', 'uridjdh', 'notes_docs/files_txt/20170831/1_291838733_uridjdh.txt', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus in nisl quis lectus sagittis ullamcorper at faucibus urna. Suspendisse tristique arcu sit amet ligula cursus pretium vitae eu elit. Nullam sed dolor ornare ex lobortis posuere. Quisque venenatis laoreet diam, in imperdiet arcu fermentum eu. Aenean molestie ligula id sem ultricies aliquet non a velit. Proin suscipit interdum vulputate. Nullam finibus gravida est, et fermentum est cursus eu. Integer sed metus ac urna molestie finibus. Aenean hendrerit ante quis diam ultrices pellentesque. Duis luctus turpis id ipsum dictum accumsan. Curabitur ornare nisi ligula, non pretium nulla venenatis sed. Aenean pharetra odio nec mi aliquam molestie. Fusce a condimentum nisl. Quisque mattis, nulla suscipit condimentum finibus, leo ex eleifend felis, vel efficitur eros turpis nec sem. ');
    [1] => INSERT INTO `mytable` (id, key, value, path, content)
                            VALUES
(NULL, '1_482737439', 'a8weele', 'notes_docs/files_txt/20170831/1_482737439_a8weele.txt', 'Nunc et odio sed odio rhoncus venenatis congue non nulla. Aliquam dictum, felis ac aliquam luctus, purus mi dignissim magna, vitae pharetra risus elit ac mi. Sed sodales dui semper commodo iaculis. Nunc vitae neque ut arcu gravida commodo. Fusce feugiat velit et felis pharetra posuere sit amet sit amet neque. Phasellus iaculis turpis odio, non consequat nunc consectetur a. Praesent ornare nisi non accumsan bibendum. Nunc vel ultricies enim, consectetur fermentum nisl. Sed eu augue ac massa efficitur ullamcorper. Ut hendrerit nisi arcu, a sagittis velit viverra ac. Quisque cursus nunc ac tincidunt sollicitudin. Cras eu rhoncus ante, ac varius velit. Mauris nibh lorem, viverra in porttitor at, interdum vel elit. Aliquam imperdiet lacus eu mi tincidunt volutpat. Vestibulum ut dolor risus. ');
    [2] => INSERT INTO `mytable` (id, key, value, path, content)
                            VALUES
(NULL, '1_579374743', 'abc2_file', 'notes_docs/files_txt/20170831/1_579374743_abc2_file.txt', 'Vivamus aliquet id elit vitae blandit. Proin laoreet ipsum sed tincidunt commodo. Fusce faucibus quam quam, in ornare ex fermentum et. Suspendisse dignissim, tortor at fringilla tempus, nibh lacus pretium metus, vel tempus dolor tellus ac orci. Vestibulum in congue dolor, nec porta elit. Donec pellentesque, neque sed commodo blandit, augue sapien dapibus arcu, sit amet hendrerit felis libero id ante. Praesent vitae elit at eros faucibus volutpat. Integer rutrum augue laoreet ex porta, ut faucibus elit accumsan. Donec in neque sagittis, auctor diam ac, viverra diam. Phasellus vel quam dolor. Nullam nisi tellus, faucibus a finibus et, blandit ac nisl. Vestibulum interdum malesuada sem, nec semper mi placerat quis. Nullam non bibendum sem, vitae elementum metus. Donec non ipsum quis turpis semper lobortis.');
    [3] => INSERT INTO `mytable` (id, key, value, path, content)
                            VALUES
(NULL, '1_733839474', 'dejsde', 'notes_docs/files_txt/20170831/1_733839474_dejsde.txt', 'Nunc faucibus, enim non luctus rutrum, lorem urna finibus turpis, sit amet dictum turpis ipsum pharetra ex. Donec at leo vitae massa consectetur viverra eget vel diam. Sed in neque tempor, vulputate quam sed, ullamcorper nisl. Fusce mollis libero in metus tincidunt interdum. Cras tempus porttitor nunc nec dapibus. Vestibulum condimentum, nisl eget venenatis tincidunt, nunc sem placerat dui, quis luctus nisl erat sed orci. Maecenas maximus finibus magna in facilisis. Maecenas maximus turpis eget dignissim fermentum. ');
    [4] => INSERT INTO `mytable` (id, key, value, path, content)
                            VALUES
(NULL, '1_837837472', 'abc_file', 'notes_docs/files_txt/20170831/1_837837472_abc_file.txt', 'Integer non ex condimentum, aliquet lectus id, accumsan nibh. Quisque aliquet, ante vitae convallis ullamcorper, velit diam tempus diam, et accumsan metus eros at tellus. Sed lacinia mauris sem, scelerisque efficitur mauris aliquam a. Nullam non auctor leo. In mattis mauris eu blandit varius. Phasellus interdum mi nec enim imperdiet tristique. In nec porttitor erat, tempor malesuada ante. Etiam scelerisque ligula et ex maximus, placerat consequat nunc consectetur. Phasellus suscipit ligula sed elit hendrerit laoreet. Suspendisse ex sem, placerat pharetra ligula eu, accumsan rhoncus ex. Sed luctus nisi vitae metus maximus scelerisque. Suspendisse porta nibh eget placerat tempus. Nunc efficitur gravida sagittis. ');
)

我会先尝试创建一个简单的数据库。主要 table 将包含文件名和文本内容。通过这种方式,您可以利用数据库引擎的查询功能,并且可能会比当前解决方案在性能上有实质性的飞跃。

这将是最简单的解决方案,它可以通过添加互补的 tables 与文件名相关的词来扩展(这可能是动态完成的,例如在最频繁的搜索中)