artvys / search
用 PHP 编写的简单搜索库
Requires
- php: ^8.1
Requires (Dev)
- phpstan/phpstan: ^1.9
- phpunit/phpunit: ^10.0
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 个概念组成。它们是 SearchEngine、SearchResult、SearchSource 和简单的查询语言。
4.1 SearchEngine
实现了《SearchEngine》接口的对象充当门面。内部的搜索、过滤和聚合逻辑可能非常复杂,并被隐藏在《SearchEngine》之后。此软件包包含一个《CompiledSearchEngine》,它使用简单的查询语言从不同的来源聚合《SearchResults》。
当调用 search
方法时,它将启动一个 Compiler 的新实例,该实例负责将用户查询编译成令牌并选择 SearchSources。请注意,《Compiler》是一个接口,因此查询语言可以被您喜欢的任何一种完全替换。
4.2 《SearchResult》
所有《SearchEngines》和所有《SearchSources》都应该返回《SearchResult》实例的数组。
此软件包决定使用一个具体的《SearchResult》实现。它是一个通用对象,旨在提供您可以从搜索结果中期待得到的常见功能。它提供流畅的接口,因此您可以轻松地构建它。
除了它所包含的常规属性外,它还提供了 Breadcrumbs、Tags 和 Links。您可以使用这些功能对搜索结果进行分类,并向用户提供辅助信息。
4.3 《SearchSource》
实际提供《SearchResults》的是称为《SearchSources》的实际提供者。它们负责接收编译后的查询并返回《SearchResults》数组。
它同样是一个接口,所以我们不关心《SearchResults》的来源。它们可以从数据库、外部API、文件系统等地方来。与这个软件包的集成大部分将集中在实现您自己的《SearchSources》子类上。提供了两个内置子类:`StaticSearchSource` 和 `FieldSearchSource`。它们是抽象类,旨在被扩展,并实现了样板代码,因此您可以专注于手头的任务。
4.4 查询语言
本包允许您使用由 编译器 实现的简单查询语言,来缩小您的 搜索结果。该语言允许用户可选地指定别名列表,然后是搜索查询。
最基本的查询没有任何特殊参数。例如
Foo bar
未指定任何别名,因此此查询将转换为标记 Foo
、Bar
并发送到已在 SearchSources 注册的参数 $allowUnaliased
值为 true
的 SearchSourceRegistry。
现在让我们解释别名。当您注册您的 SearchSources 时,您可以给它们赋予别名。这些是用于识别它们的简单名称。然后用户可以使用这些别名来限制搜索范围。然后是之前提到的参数 $allowUnaliased
。它控制是否可以查询 SearchSource,而无需强制用户明确输入别名。对于慢速外部API或通常会杂乱的结果来源,这可能很有用。
根据前面的配置示例,让我们回到最简单的查询
Foo bar
看看这些 SearchSources 的注册方式。查询中没有别名,所以将使用 UsersSearchSource
和 InvoicesSearchSource
。
要使用别名,只需在查询开始处加上它
@foo bar
我们有一个别名 @
,它将被解析为 UsersSearchSource
和标记 foo
、bar
。因此,将只使用这两个标记查询 UsersSearchSource
。
您甚至可以将多个别名组合在一起
#@foo bar
此查询将被解析为 InvoicesSearchSource
和 UsersSearchSource
。解析的来源顺序与查询顺序相匹配。
现在让我们关注单字符和多字符别名的区别。单字符别名可以不使用分隔符组合,因为它们是唯一的且易于识别。多字符别名则不是。它们需要使用逗号分隔,否则您将得到一个更大的别名。因此,让我们通过示例展示几乎所有可能的格式
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
如果您想了解查询语言的工作原理,请确保查看 Lexer 和 Parser 的实现。
5. 实现 SearchSource
您的大部分工作将集中在实现自己的搜索源。单独的搜索源是一个接口,但此包提供了两个抽象基类以简化您的集成:StaticSearchSource,FieldSearchSource。更多源可以在第三方适配器中找到。
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. 第三方适配器
- WIP: artvys/cake-php - artvys/search包的CakePHP适配器