artvys/search

用 PHP 编写的简单搜索库

1.0.0 2023-04-25 15:44 UTC

This package is auto-updated.

Last update: 2024-09-25 19:05:17 UTC


README

这是一个用 PHP 编写,没有外部依赖的简单搜索库。此包提供了搜索功能,同时与框架无关。可以使用特定于第一方框架的包来简化集成过程。

1. 安装

本节描述了该包的安装过程。如果您想安装特定于框架的适配器,请先阅读其安装说明。如果您已安装 composer,则只需运行

composer require artvys/search

2. 配置

要使用此包,首先您需要组装一个 SearchEngine。您可以在现场这样做

use Your\Project\SearchSources\UsersSearchSource;
use Your\Project\SearchSources\InvoicesSearchSource;
use Your\Project\SearchSources\ExternalAPISearchSource;

use Artvys\Search\Engines\Compiled\SearchSourceRegistry;
use Artvys\Search\Engines\Compiled\Compilers\IO\IOCompilerFactory;
use Artvys\Search\Engines\Compiled\FetchingStrategies\FirstFitFetchingStrategy;
use Artvys\Search\Engines\Compiled\CompiledSearchEngine;

$registry = new SearchSourceRegistry();
$registry->register(new UsersSearchSource(), ['@']);
$registry->register(new InvoicesSearchSource(), ['#']);
$registry->register(new ExternalAPISearchSource(), ['api'], false);
$compilerFactory = new IOCompilerFactory($registry);
$fetchingStrategy = new FirstFitFetchingStrategy();
$engine = new CompiledSearchEngine($compilerFactory, $fetchingStrategy);

但通常您会在某种依赖注入容器中一次性配置它,然后让容器构建它

use Your\Project\SearchSources\UsersSearchSource;
use Your\Project\SearchSources\InvoicesSearchSource;
use Your\Project\SearchSources\ExternalAPISearchSource;

use Artvys\Search\Engines\Compiled\SearchSourceRegistry;
use Artvys\Search\Engines\Compiled\Compilers\IO\IOCompilerFactory;
use Artvys\Search\Engines\Compiled\FetchingStrategies\FirstFitFetchingStrategy;
use Artvys\Search\Engines\Compiled\CompiledSearchEngine;
use Artvys\Search\Engines\Compiled\Compilers\IO\SearchSourceProvider;
use Artvys\Search\SearchEngine;

$container->add(SearchSourceProvider::class, function() {
    $registry = new SearchSourceRegistry();
    $registry->register(new UsersSearchSource(), ['@']);
    $registry->register(new InvoicesSearchSource(), ['#']);
    $registry->register(new ExternalAPISearchSource(), ['api'], false);

    return $registry;
});

$container->add(SearchEngine::class, function($c) {
    $compilerFactory = new IOCompilerFactory($c->get(SearchSourceProvider::class));
    $fetchingStrategy = new FirstFitFetchingStrategy();

    return new CompiledSearchEngine($compilerFactory, $fetchingStrategy);
});

3. 使用

要获取搜索结果,只需在 SearchEngine 上调用 search 方法。鉴于前面部分构建的 SearchEngine,您可以执行以下操作。

$results = $engine->search('Foo bar', 10);

搜索引擎将查找最多 10 个匹配查询 "Foo bar" 的 SearchResults

让我们看看一个可能由您的项目需要的实际用例

use Artvys\Search\SearchEngine;

class SearchController {
    public SearchEngine $searchEngine;

    public function __construct(SearchEngine $searchEngine) {
        $this->searchEngine = $searchEngine;
    }

    public function search(Request $request): Response {
        return new JsonResponse($this->searchEngine->search($request->query('search'), $request->query('limit', 10)));
    }
}

此示例说明了如何在 Controller 类中使用 SearchEngine。建议更好的输入处理。通过依赖注入容器将 SearchEngine 注入到 SearchController 中。我们从请求中获取用户查询并指定限制(默认为 10)。SearchEngine 将查找 SearchResults 并将它们传递给 JsonResponse。默认情况下,SearchResult 实现 JsonSerializable 接口,以便轻松集成。

4. 核心概念

要充分利用此包,您只需学习它由 4 个概念组成。它们是 SearchEngineSearchResultSearchSource 和简单的查询语言。

4.1 SearchEngine

实现了《SearchEngine》接口的对象充当门面。内部的搜索、过滤和聚合逻辑可能非常复杂,并被隐藏在《SearchEngine》之后。此软件包包含一个《CompiledSearchEngine》,它使用简单的查询语言从不同的来源聚合《SearchResults》。

当调用 search 方法时,它将启动一个 Compiler 的新实例,该实例负责将用户查询编译成令牌并选择 SearchSources。请注意,《Compiler》是一个接口,因此查询语言可以被您喜欢的任何一种完全替换。

4.2 《SearchResult

所有《SearchEngines》和所有《SearchSources》都应该返回《SearchResult》实例的数组。

此软件包决定使用一个具体的《SearchResult》实现。它是一个通用对象,旨在提供您可以从搜索结果中期待得到的常见功能。它提供流畅的接口,因此您可以轻松地构建它。

除了它所包含的常规属性外,它还提供了 BreadcrumbsTagsLinks。您可以使用这些功能对搜索结果进行分类,并向用户提供辅助信息。

4.3 《SearchSource

实际提供《SearchResults》的是称为《SearchSources》的实际提供者。它们负责接收编译后的查询并返回《SearchResults》数组。

它同样是一个接口,所以我们不关心《SearchResults》的来源。它们可以从数据库、外部API、文件系统等地方来。与这个软件包的集成大部分将集中在实现您自己的《SearchSources》子类上。提供了两个内置子类:`StaticSearchSource` 和 `FieldSearchSource`。它们是抽象类,旨在被扩展,并实现了样板代码,因此您可以专注于手头的任务。

4.4 查询语言

本包允许您使用由 编译器 实现的简单查询语言,来缩小您的 搜索结果。该语言允许用户可选地指定别名列表,然后是搜索查询。

最基本的查询没有任何特殊参数。例如

Foo bar

未指定任何别名,因此此查询将转换为标记 FooBar 并发送到已在 SearchSources 注册的参数 $allowUnaliased 值为 trueSearchSourceRegistry

现在让我们解释别名。当您注册您的 SearchSources 时,您可以给它们赋予别名。这些是用于识别它们的简单名称。然后用户可以使用这些别名来限制搜索范围。然后是之前提到的参数 $allowUnaliased。它控制是否可以查询 SearchSource,而无需强制用户明确输入别名。对于慢速外部API或通常会杂乱的结果来源,这可能很有用。

根据前面的配置示例,让我们回到最简单的查询

Foo bar

看看这些 SearchSources 的注册方式。查询中没有别名,所以将使用 UsersSearchSourceInvoicesSearchSource

要使用别名,只需在查询开始处加上它

@foo bar

我们有一个别名 @,它将被解析为 UsersSearchSource 和标记 foobar。因此,将只使用这两个标记查询 UsersSearchSource

您甚至可以将多个别名组合在一起

#@foo bar

此查询将被解析为 InvoicesSearchSourceUsersSearchSource。解析的来源顺序与查询顺序相匹配。

现在让我们关注单字符和多字符别名的区别。单字符别名可以不使用分隔符组合,因为它们是唯一的且易于识别。多字符别名则不是。它们需要使用逗号分隔,否则您将得到一个更大的别名。因此,让我们通过示例展示几乎所有可能的格式

foo bar             //                    | Tokens: foo bar
api foo bar         // Aliases: api       | Tokens: foo bar
api:foo bar         // Aliases: api       | Tokens: foo bar
api: foo bar        // Aliases: api       | Tokens: foo bar
@foo bar            // Aliases: @         | Tokens: foo bar
@# foo bar          // Aliases: @, #      | Tokens: foo bar
@,#,api: foo bar    // Aliases: @, #, api | Tokens: foo bar
#@,api foo bar      // Aliases: #, @, api | Tokens: foo bar

一个不错的小功能是可以为多个 SearchSources 使用相同的别名。例如,如果您想使用别名 # 搜索发票和票证,只需按如下方式注册它们

use Your\Project\SearchSources\TicketsSearchSource;
use Your\Project\SearchSources\InvoicesSearchSource;

use Artvys\Search\Engines\Compiled\SearchSourceRegistry;

$registry = new SearchSourceRegistry();
$registry->register(new TicketsSearchSource(), ['#']);
$registry->register(new InvoicesSearchSource(), ['#']);

并按如下方式查询它们

#12345

如果您想了解查询语言的工作原理,请确保查看 LexerParser 的实现。

5. 实现 SearchSource

您的大部分工作将集中在实现自己的搜索源。单独的搜索源是一个接口,但此包提供了两个抽象基类以简化您的集成:StaticSearchSourceFieldSearchSource。更多源可以在第三方适配器中找到。

StaticSearchSource适用于静态的搜索结果。也许您有静态页面并希望它们可搜索?只需扩展StaticSearchSource基类并实现allResults方法。只需yield所有可能的结果,基类就会处理剩下的工作。您可以通过重写fields方法来自定义用于搜索的属性。

use Artvys\Search\Engines\Compiled\SearchSources\Static\StaticSearchSource;
use Artvys\Search\SearchResult;
use Generator;

class UsersNavigationSearchSource extends StaticSearchSource {
    protected function allResults(): Generator {
        yield SearchResult::make('All users', 'Index page for all Users.', 'https://foo.bar');
        yield SearchResult::make('Create a users', 'Page for the creation of a new user.', 'https://foo.bar');
    }
}

FieldSearchSource使用简单的声明,指定哪些字段应用于搜索。您需要实现2个方法。第一个是fields,在其中指定您想要使用的字段。第二个是makeResultQueryBuilder,您只需返回一个ResultQueryBuilder实例。上述接口的实现由第三方适配器提供。

use Your\Project\DBResultQueryBuilder;

use Artvys\Search\Engines\Compiled\SearchSources\Field\FieldSearchSource;
use Artvys\Search\Engines\Compiled\SearchSources\Field\SearchFieldBuilder;
use Artvys\Search\Engines\Compiled\CompiledQuery;
use Artvys\Search\Engines\Compiled\SearchSources\Field\Field;
use Artvys\Search\Engines\Compiled\SearchSources\Field\ResultQueryBuilder;

class UsersSearchSource extends FieldSearchSource {
    protected function fields(SearchFieldBuilder $builder, CompiledQuery $query, int $limit): void {
        $builder->add(Field::contains('name'))
            ->add(Field::contains('description'));
    }

    protected function makeResultQueryBuilder(): ResultQueryBuilder {
        return new DBResultQueryBuilder('users');
    }
}

6. 扩展

此包使用大量接口和抽象以尽可能灵活。您基本上可以扩展或替换任何内容。扩展现有类,或在需要更多控制时直接实现接口。然后只需更新构建逻辑,通常在依赖注入容器内部。

让我们通过一个缓存示例来说明。立即,有两种选择呈现在我们面前。我们可以缓存整个结果集或单个源。前者可能会在性能方面带来更好的结果,但代价是复杂的缓存失效。后者提供了更大的灵活性。例如,您可以仅缓存来自外部API的结果。

让我们从第一个示例开始,并使用装饰器模式实现缓存的搜索引擎

use Artvys\Search\SearchEngine;

class CachedSearchEngine implements SearchEngine {
    private SearchEngine $engine;
    private Cache $cache;
    private CacheKeyGenerator $cacheKeyGenerator;
    private int $expiresInSeconds;

    public function __construct(SearchEngine $engine, Cache $cache, CacheKeyGenerator $cacheKeyGenerator, int $expiresInSeconds = 3600) {
        $this->engine = $engine;
        $this->cache = $cache;
        $this->cacheKeyGenerator = $cacheKeyGenerator;
        $this->expiresInSeconds = $expiresInSeconds;
    }

    public function search(string $query, int $limit): array {
        $key = $this->cacheKeyGenerator->generate($query, $limit);

        if (!$this->cache->has($key)) {
            $results = $this->engine->search($query, $limit);
            $this->cache->remember($key, $results, $this->expiresInSeconds);
        }

        return $this->cache->get($key);
    }
}

然后只需调整构建逻辑

use Your\Project\CachedSearchEngine;

use Artvys\Search\Engines\Compiled\CompiledSearchEngine;

$engine = new CachedSearchEngine(
    new CompiledSearchEngine(...),
    new Cache(...),
    new CacheKeyGenerator(...),
    600
);

我们将为第二个示例做类似的事情。我们再次利用装饰器模式来实现缓存的搜索源

use \Artvys\Search\Engines\Compiled\SearchSource;

class CachedSearchSource implements SearchSource {
    private SearchSource $source;
    private Cache $cache;
    private CacheKeyGenerator $cacheKeyGenerator;
    private int $expiresInSeconds;

    public function __construct(SearchSource $source, Cache $cache, CacheKeyGenerator $cacheKeyGenerator, int $expiresInSeconds = 3600) {
        $this->source = $source;
        $this->cache = $cache;
        $this->cacheKeyGenerator = $cacheKeyGenerator;
        $this->expiresInSeconds = $expiresInSeconds;
    }
    public function search(CompiledQuery $query, int $limit): array {
        $key = $this->cacheKeyGenerator->generate((string)$query, $limit);

        if (!$this->cache->has($key)) {
            $results = $this->source->search($query, $limit);
            $this->cache->remember($key, $results, $this->expiresInSeconds);
        }

        return $this->cache->get($key);
    }
}

几乎一样。现在只需展示用法

use Your\Project\SearchSources\UsersSearchSource;

use Artvys\Search\Engines\Compiled\SearchSourceRegistry;

$registry = new SearchSourceRegistry();
$registry->register(new CachedSearchSource(new UsersSearchSource()), ['#']);

7. 第三方适配器