PHP应用中的Search Engine集成:Elasticsearch、Solr与Algolia的查询 DSL 封装

好的,我们开始。

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 查询,添加 mustshouldfilter 条件,以及构建 matchtermrange 查询。 build() 方法返回最终的查询数组, toJson() 方法将其转换为JSON字符串。

2. 更高级的查询构建:

我们可以扩展 ElasticsearchQueryBuilder 类,添加更多的方法来支持更复杂的查询,例如 multi_matchprefixwildcard 等。

<?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);

这个类添加了 multiMatchprefixwildcardexistsnested 等方法,使我们可以构建更复杂的查询。

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);
    }
}

这个类添加了断言来验证 matchrange 方法的参数。如果参数不符合要求,断言会抛出一个异常。

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 类,添加更多的方法来支持更复杂的查询,例如 dismaxedismax 等。

<?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

这个类添加了 dismaxedismax 方法,使我们可以构建更复杂的查询。

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格式的查询参数。通过类型安全和验证机制,我们可以进一步提高代码的健壮性。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注