好的,我们开始。
PHP应用中的Search Engine集成:Elasticsearch、Solr与Algolia的查询 DSL 封装
大家好,今天我们来聊聊PHP应用中集成搜索引擎,特别是Elasticsearch、Solr和Algolia这三个流行的搜索引擎,以及如何对它们的查询DSL(Domain Specific Language)进行封装,以方便我们在PHP代码中使用。
为什么需要封装查询DSL?
在PHP应用中直接拼接字符串来构建Elasticsearch、Solr或Algolia的查询DSL是很常见的做法,但这种方式存在诸多问题:
- 可读性差: 复杂的查询语句会变得难以理解和维护。
- 容易出错: 手动拼接字符串容易出现语法错误,调试困难。
- 安全性问题: 未经处理的用户输入直接拼接到查询语句中可能导致注入攻击。
- 代码复用性差: 相同的查询逻辑需要在多个地方重复编写。
因此,我们需要一种更优雅、更安全、更易于维护的方式来构建查询语句。这就是查询DSL封装的目的。通过封装,我们可以将复杂的查询逻辑抽象成易于使用的PHP对象或函数,提高代码的可读性、可维护性和安全性。
Elasticsearch 查询 DSL 封装
Elasticsearch的查询DSL基于JSON,非常灵活但也相对复杂。我们可以使用PHP的数组来表示JSON结构,然后将其转换为JSON字符串发送给Elasticsearch。
1. 基本的查询构建器:
首先,我们创建一个 ElasticsearchQueryBuilder 类,用于构建基本的查询结构。
<?php
class ElasticsearchQueryBuilder
{
private array $query = [];
public function bool(): self
{
$this->query['bool'] = [];
return $this;
}
public function must(array $conditions): self
{
if (!isset($this->query['bool'])) {
$this->bool();
}
$this->query['bool']['must'] = $conditions;
return $this;
}
public function should(array $conditions): self
{
if (!isset($this->query['bool'])) {
$this->bool();
}
$this->query['bool']['should'] = $conditions;
return $this;
}
public function filter(array $conditions): self
{
if (!isset($this->query['bool'])) {
$this->bool();
}
$this->query['bool']['filter'] = $conditions;
return $this;
}
public function match(string $field, string $value): array
{
return ['match' => [$field => $value]];
}
public function term(string $field, string $value): array
{
return ['term' => [$field => $value]];
}
public function range(string $field, array $range): array
{
return ['range' => [$field => $range]];
}
public function build(): array
{
return $this->query;
}
public function toJson(): string
{
return json_encode($this->build());
}
}
// Example usage:
$builder = new ElasticsearchQueryBuilder();
$query = $builder->bool()
->must([
$builder->match('title', 'Elasticsearch'),
$builder->term('status', 'published')
])
->filter([
$builder->range('publish_date', ['gte' => '2023-01-01'])
])
->build();
echo json_encode($query, JSON_PRETTY_PRINT);
//output:
//{
// "bool": {
// "must": [
// {
// "match": {
// "title": "Elasticsearch"
// }
// },
// {
// "term": {
// "status": "published"
// }
// }
// ],
// "filter": [
// {
// "range": {
// "publish_date": {
// "gte": "2023-01-01"
// }
// }
// }
// ]
// }
//}
这个类提供了一些基本的方法,用于构建 bool 查询,添加 must、should 和 filter 条件,以及构建 match、term 和 range 查询。 build() 方法返回最终的查询数组, toJson() 方法将其转换为JSON字符串。
2. 更高级的查询构建:
我们可以扩展 ElasticsearchQueryBuilder 类,添加更多的方法来支持更复杂的查询,例如 multi_match、prefix、wildcard 等。
<?php
// Extend the ElasticsearchQueryBuilder class
class AdvancedElasticsearchQueryBuilder extends ElasticsearchQueryBuilder
{
public function multiMatch(array $fields, string $query, string $type = 'best_fields'): array
{
return ['multi_match' => ['query' => $query, 'fields' => $fields, 'type' => $type]];
}
public function prefix(string $field, string $prefix): array
{
return ['prefix' => [$field => ['value' => $prefix]]];
}
public function wildcard(string $field, string $pattern): array
{
return ['wildcard' => [$field => ['value' => $pattern]]];
}
public function exists(string $field): array
{
return ['exists' => ['field' => $field]];
}
public function nested(string $path, array $query): array
{
return ['nested' => ['path' => $path, 'query' => $query]];
}
}
// Example usage:
$builder = new AdvancedElasticsearchQueryBuilder();
$query = $builder->bool()
->must([
$builder->multiMatch(['title', 'description'], 'Elasticsearch tutorial'),
$builder->prefix('author', 'John')
])
->filter([
$builder->exists('publish_date')
])
->should([
$builder->wildcard('category', 'dev*')
])
->build();
echo json_encode($query, JSON_PRETTY_PRINT);
这个类添加了 multiMatch、prefix、wildcard、exists 和 nested 等方法,使我们可以构建更复杂的查询。
3. 使用示例:
<?php
// Suppose you have an Elasticsearch client instance:
//$client = ElasticsearchClientBuilder::create()->build();
// Build the query using the builder:
$builder = new AdvancedElasticsearchQueryBuilder();
$query = $builder->bool()
->must([
$builder->match('title', 'Elasticsearch')
])
->filter([
$builder->range('price', ['gte' => 10, 'lte' => 100])
])
->build();
// Execute the search:
//$params = [
// 'index' => 'products',
// 'body' => ['query' => $query]
//];
//
//$response = $client->search($params);
// Process the results:
//print_r($response);
这段代码展示了如何使用构建器来构建查询,并将其发送给Elasticsearch客户端。请注意,这里只是示例代码,你需要根据你的实际情况配置Elasticsearch客户端。
4. 类型安全与验证:
为了提高代码的健壮性,我们可以添加类型安全和验证机制。例如,我们可以使用类型提示来确保方法的参数类型正确,使用断言来验证参数的取值范围。
<?php
class TypeSafeElasticsearchQueryBuilder extends ElasticsearchQueryBuilder
{
public function match(string $field, string $value): array
{
assert(!empty($field), 'Field cannot be empty.');
assert(is_string($value), 'Value must be a string.');
return parent::match($field, $value);
}
public function range(string $field, array $range): array
{
assert(!empty($field), 'Field cannot be empty.');
assert(is_array($range), 'Range must be an array.');
assert(isset($range['gte']) || isset($range['lte']), 'Range must have gte or lte.');
return parent::range($field, $range);
}
}
这个类添加了断言来验证 match 和 range 方法的参数。如果参数不符合要求,断言会抛出一个异常。
Solr 查询 DSL 封装
Solr的查询语法基于Lucene查询语法,与Elasticsearch的JSON DSL不同。Solr查询字符串更像是一种高级的SQL语句,需要不同的封装策略。
1. 基本的查询构建器:
<?php
class SolrQueryBuilder
{
private array $params = [];
public function query(string $q): self
{
$this->params['q'] = $q;
return $this;
}
public function filterQuery(string $fq): self
{
if (!isset($this->params['fq'])) {
$this->params['fq'] = [];
}
$this->params['fq'][] = $fq;
return $this;
}
public function facet(string $field, array $options = []): self
{
$this->params['facet'] = 'true';
$this->params['facet.field'] = $field;
foreach ($options as $key => $value) {
$this->params["facet.{$key}"] = $value;
}
return $this;
}
public function sortBy(string $field, string $direction = 'asc'): self
{
$this->params['sort'] = $field . ' ' . $direction;
return $this;
}
public function start(int $start): self
{
$this->params['start'] = $start;
return $this;
}
public function rows(int $rows): self
{
$this->params['rows'] = $rows;
return $this;
}
public function build(): array
{
return $this->params;
}
public function toString(): string
{
return http_build_query($this->build());
}
}
// Example usage:
$builder = new SolrQueryBuilder();
$query = $builder->query('title:Elasticsearch')
->filterQuery('status:published')
->facet('category', ['limit' => 10])
->sortBy('publish_date', 'desc')
->start(0)
->rows(20)
->toString();
echo $query;
// Output: q=title%3AElasticsearch&fq%5B0%5D=status%3Apublished&facet=true&facet.field=category&facet.limit=10&sort=publish_date+desc&start=0&rows=20
这个类提供了一些基本的方法,用于构建查询字符串,添加过滤条件、facet、排序、分页等参数。 build() 方法返回参数数组, toString() 方法将其转换为URL编码的查询字符串。
2. 更高级的查询构建:
我们可以扩展 SolrQueryBuilder 类,添加更多的方法来支持更复杂的查询,例如 dismax、edismax 等。
<?php
class AdvancedSolrQueryBuilder extends SolrQueryBuilder
{
public function dismax(string $query, array $options = []): self
{
$this->params['defType'] = 'dismax';
$this->params['q'] = $query;
foreach ($options as $key => $value) {
$this->params[$key] = $value;
}
return $this;
}
public function edismax(string $query, array $options = []): self
{
$this->params['defType'] = 'edismax';
$this->params['q'] = $query;
foreach ($options as $key => $value) {
$this->params[$key] = $value;
}
return $this;
}
}
// Example usage:
$builder = new AdvancedSolrQueryBuilder();
$query = $builder->edismax('Elasticsearch tutorial', [
'qf' => 'title^2 description',
'mm' => '2<-1 5<-2 6<90%',
'pf' => 'title description'
])->toString();
echo $query;
// Output: defType=edismax&q=Elasticsearch+tutorial&qf=title%5E2+description&mm=2%3C-1+5%3C-2+6%3C90%25&pf=title+description
这个类添加了 dismax 和 edismax 方法,使我们可以构建更复杂的查询。
3. 使用示例:
<?php
// Suppose you have a Solr client instance:
//$client = new SolariumClient($config);
// Build the query using the builder:
$builder = new AdvancedSolrQueryBuilder();
$queryString = $builder->query('title:Elasticsearch')
->filterQuery('category:tutorial')
->sortBy('score', 'desc')
->toString();
// Create a select query instance
//$query = $client->createSelect();
//$query->setQuery($queryString);
// Execute the query
//$resultset = $client->select($query);
// Display the result count
//echo 'NumFound: '.$resultset->getNumFound();
// Show the documents
//foreach ($resultset as $document) {
// echo '<hr/><table>';
// foreach ($document as $field => $value) {
// echo '<tr><th>' . $field . '</th><td>' . $value . '</td></tr>';
// }
// echo '</table>';
//}
这段代码展示了如何使用构建器来构建查询字符串,并将其发送给Solr客户端。请注意,这里只是示例代码,你需要根据你的实际情况配置Solr客户端。
Algolia 查询 DSL 封装
Algolia提供了一个REST API,我们可以使用PHP的HTTP客户端来与其交互。Algolia的查询参数也需要进行封装。
1. 基本的查询构建器:
<?php
class AlgoliaQueryBuilder
{
private array $params = [];
public function query(string $query): self
{
$this->params['query'] = $query;
return $this;
}
public function filters(string $filters): self
{
$this->params['filters'] = $filters;
return $this;
}
public function facetFilters(array $facetFilters): self
{
$this->params['facetFilters'] = $facetFilters;
return $this;
}
public function numericFilters(array $numericFilters): self
{
$this->params['numericFilters'] = $numericFilters;
return $this;
}
public function hitsPerPage(int $hitsPerPage): self
{
$this->params['hitsPerPage'] = $hitsPerPage;
return $this;
}
public function page(int $page): self
{
$this->params['page'] = $page;
return $this;
}
public function build(): array
{
return $this->params;
}
public function toJson(): string
{
return json_encode($this->build());
}
}
// Example usage:
$builder = new AlgoliaQueryBuilder();
$query = $builder->query('Elasticsearch')
->filters('status = published')
->facetFilters([['category:tutorial']])
->numericFilters(['price >= 10'])
->hitsPerPage(20)
->page(0)
->toJson();
echo $query;
// Output: {"query":"Elasticsearch","filters":"status = published","facetFilters":[["category:tutorial"]],"numericFilters":["price >= 10"],"hitsPerPage":20,"page":0}
这个类提供了一些基本的方法,用于构建查询参数,添加过滤条件、facet、分页等参数。 build() 方法返回参数数组, toJson() 方法将其转换为JSON字符串。
2. 使用示例:
<?php
use AlgoliaAlgoliaSearchSearchClient;
// Initialize Algolia client
//$client = SearchClient::create('YOUR_APP_ID', 'YOUR_API_KEY');
//$index = $client->initIndex('your_index_name');
// Build the query using the builder:
$builder = new AlgoliaQueryBuilder();
$params = $builder->query('Elasticsearch')
->filters('category:tutorial')
->hitsPerPage(10)
->build();
// Search the index
//$results = $index->search('', $params);
// Print the results
//print_r($results);
这段代码展示了如何使用构建器来构建查询参数,并将其发送给Algolia客户端。请注意,这里只是示例代码,你需要根据你的实际情况配置Algolia客户端。
总结:DSL封装提升开发效率
通过封装查询DSL,我们可以将复杂的查询逻辑抽象成易于使用的PHP对象或函数,提高代码的可读性、可维护性和安全性。针对不同的搜索引擎,我们需要采用不同的封装策略。对于Elasticsearch,我们可以使用PHP数组来表示JSON结构;对于Solr,我们可以构建URL编码的查询字符串;对于Algolia,我们可以构建JSON格式的查询参数。通过类型安全和验证机制,我们可以进一步提高代码的健壮性。