s2/rose

PHP 网站的搜索引擎

v3.0.0 2024-08-24 13:14 UTC

README

这是一个为具有简化但功能性的英语和俄语形态学支持的网站内容设计的搜索引擎。它索引您的内容并提供全文搜索。

需求

  1. PHP 7.4 或更高版本。
  2. 如果内容量很大,需要使用关系型数据库。支持的数据库有

安装

composer require s2/rose

使用

准备存储

索引可以存储在数据库中或文件中。存储作为一个抽象层,隐藏了实现细节。在大多数情况下,您需要数据库存储 PdoStorage

存储对于索引和搜索都是必需的。

$pdo = new \PDO('mysql:host=127.0.0.1;dbname=s2_rose_test;charset=utf8', 'username', 'passwd');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);

use S2\Rose\Storage\Database\PdoStorage;

$storage = new PdoStorage($pdo, 'table_prefix_');

当您想要重建索引时,调用 PdoStorage::erase() 方法

$storage->erase();

它会删除索引表(如果存在)并从头创建新的索引表。此方法足以升级到与现有索引不兼容的新版本的 Rose。

形态学

对于自然语言处理,Rose 使用词干提取器。词干提取器截断词的屈折部分,Rose 处理由此产生的词干。Rose 没有内置的词典,但它包括了 Porter 开发的启发式词干提取器。您可以通过实现 StemmerInterface 集成任何其他算法。

use S2\Rose\Stemmer\PorterStemmerEnglish;
use S2\Rose\Stemmer\PorterStemmerRussian;

// For optimization primary language goes first (in this case Russian)
$stemmer = new PorterStemmerRussian(new PorterStemmerEnglish());

索引

Indexer 构建搜索索引。它依赖于词干提取器和存储。

use S2\Rose\Indexer;

$indexer = new Indexer($storage, $stemmer);

索引器接受您以特殊格式提供的数据。数据必须包裹在 Indexable 类中

use S2\Rose\Entity\Indexable;

// Main parameters
$indexable = new Indexable(
    'id_1',            // External ID - an identifier in your system 
    'Test page title', // Title 
    'This is the first page to be indexed. I have to make up a content.',
    1                  // Instance ID - an optional ID of your subsystem 
);

// Other optional parameters
$indexable
    ->setKeywords('singlekeyword, multiple keywords')       // The same as Meta Keywords
    ->setDescription('Description can be used in snippets') // The same as Meta Description
    ->setDate(new \DateTime('2016-08-24 00:00:00'))
    ->setUrl('url1')
    ->setRelevanceRatio(3.14)                               // Multiplier for important pages
;

$indexer->index($indexable);

$indexable = new Indexable(
    'id_2',
    'Test page title 2',
    'This is the second page to be indexed. Let\'s compose something new.'
);
$indexable->setKeywords('content, page');

$indexer->index($indexable);

Indexable 的构造函数有 4 个参数

  • 外部 ID - 一个足以让您的代码识别页面的任意字符串;
  • 页面标题;
  • 页面内容;
  • 实例 ID - 页面源的可选整数 ID(例如,用于多站服务),如下所述。

您可以提供的可选参数包括:关键词、描述、日期、相关性比率以及 URL。关键词以更高的相关性进行索引和搜索。描述可用于构建摘要(见下文)。建议使用“关键词”和“描述”元标签的内容,如果有的话,用于此目的。URL 可以是任意字符串。

使用 Indexer::index() 方法添加和更新索引。如果内容未更改,则此方法跳过操作。否则,内容将被删除并重新索引。

当您从网站中删除页面时,只需调用

$indexer->removeById($externalId, $instanceId);

搜索

可以通过 Finder 类获取全文搜索结果。 $resultSet->getItems() 返回有关内容项及其相关性的所有信息。

use S2\Rose\Finder;
use S2\Rose\Entity\Query;

$finder    = new Finder($storage, $stemmer);
$resultSet = $finder->find(new Query('content'));

foreach ($resultSet->getItems() as $item) {
                             // first iteration:              second iteration:
    $item->getId();          // 'id_2'                        'id_1'
    $item->getInstanceId();  // null                          1
    $item->getTitle();       // 'Test page title 2'           'Test page title'
    $item->getUrl();         // ''                            'url1'
    $item->getDescription(); // ''                            'Description can be used in snippets'
    $item->getDate();        // null                          new \DateTime('2016-08-24 00:00:00')
    $item->getRelevance();   // 4.1610856664112195            0.26907154598642522
    $item->getSnippet();     // 'This is the second page...'  'I have to make up a <i>content</i>.'
}

修改 Query 对象以使用分页

$query = new Query('content');
$query
    ->setLimit(10)  // 10 results per page
    ->setOffset(20) // third page
;
$resultSet = $finder->find($query);

$resultSet->getTotalCount(); // Returns total amount of found items (for pagination links)

提供实例 ID 以限制搜索范围的子系统

$resultSet = $finder->find((new Query('content'))->setInstanceId(1));

foreach ($resultSet->getItems() as $item) {
                             // first iteration only:
    $item->getId();          // 'id_1'
    $item->getInstanceId();  // 1
}

突出显示和摘要

在搜索结果中突出显示找到的词是一种常见做法。您可以通过以下方法获取突出显示的标题

$resultSet = $finder->find(new Query('title'));
$resultSet->getItems()[0]->getHighlightedTitle($stemmer); // 'Test page <i>title</i>'

此方法需要词干提取器,因为它考虑了形态学并突出显示所有词形。默认情况下,单词以斜体突出显示。您可以通过调用 $finder->setHighlightTemplate('<b>%s</b>') 改变突出显示模板。

摘要是由找到的词组成的小文本片段,在搜索结果页面中显示。Rose 处理索引内容并选择最佳匹配的句子。

use S2\Rose\Entity\ExternalContent;
use S2\Rose\Snippet\SnippetBuilder;

$finder->setSnippetLineSeparator(' &middot; '); // Set snippet line separator. Default is '... '.

$resultSet->getItems()[0]->getSnippet();
// 'I have to make up a <i>content</i>. &middot; I have changed the <i>content</i>.'

摘要中的单词以与标题中相同的方式突出显示。

如果构建代码片段需要花费大量时间,请尝试使用分页来减少处理的代码片段数量。

示例

示例可以帮助限制搜索范围。

例如,您可以尝试索引博客文章的instance_id = 1和评论的instance_id = 2。然后您可以使用不同限制运行查询

  • (new Query('content'))->setInstanceId(1)搜索博客文章,
  • (new Query('content'))->setInstanceId(2)搜索评论,
  • (new Query('content'))搜索所有地方。

在索引时,如果您省略了instance_id或提供了instance_id === null,则内部将使用值0。这种内容只能匹配没有instance_id限制的查询。

内容格式和提取

Rose是为网站和Web应用程序设计的。它默认支持内容的HTML格式。但是,可以扩展代码以支持其他格式(例如纯文本,Markdown)。这可以通过创建自定义提取器来完成

use S2\Rose\Extractor\ExtractorInterface;
use S2\Rose\Indexer;

class CustomExtractor implements ExtractorInterface
{
    // ...
    // Please refer to the source code
    // to figure out how to create an extractor. 
} 

$indexer = new Indexer($storage, $stemmer, new CustomExtractor(), new Logger());

建议

PdoStorage具有在整个索引集合中识别相似项的能力。

考虑一个场景,您有一个博客,并且使用Rose对其文章进行索引。这个特定功能允许您为每篇文章选择一组其他文章,使访客能够探索相关内容。

全文索引中的数据结构非常适合选择相似文章的任务。简单来说,常规搜索是根据搜索查询中的单词选择相关文章,而文章推荐是根据给定文章中的单词选择其他文章。

您可以通过调用以下方法来检索推荐

$similarItems = $readStorage->getSimilar(new ExternalId('id_2'));
// The result contains the following data:
// $similarItems[0] = [
//     'tocWithMetadata' => new TocEntryWithMetadata(...),
//     'external_id'     => 'id_1',
//     'instance_id'     => '1',
//     'title'           => 'Test page title',
//     'snippet'         => 'This is the first page to be indexed.',
//     'snippet2'        => 'I have to make up a content.',
// ],

注意

推荐在MySQL和PostgreSQL数据库上受支持。由于SQL支持有限,它们在SQLite中未实现。