Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 59 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@

[![Latest Version](https://img.shields.io/packagist/v/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit)
[![Total Downloads](https://img.shields.io/packagist/dt/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit)
[![Tests](https://github.com/ykan821/ElasticKit/actions/workflows/ci.yml/badge.svg)](https://github.com/ykan821/ElasticKit/actions/workflows/ci.yml)
[![PHP](https://img.shields.io/packagist/php-v/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit)
[![License](https://img.shields.io/packagist/l/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit)

A PHP Elasticsearch DSL query builder covering queries, aggregations, CRUD, bulk writes, and zero-downtime rebuilds.

## Integrations

- **[ElasticKit Laravel](https://github.com/ykan821/ElasticKitLaravel)** — Laravel integration
(native pagination, artisan rebuild). `composer require ykan/elastickit-laravel`.

## Installation

```
Expand Down Expand Up @@ -50,46 +57,11 @@ $total = $results->total(); // null unless $trackTotalHits = true (see Paginatio

## DSL Examples

ElasticKit's DSL stays close to the native ES API to minimize cognitive load — if you know ES DSL, the transfer is smooth. Every query type is a dedicated `Node` class whose method names mirror ES parameters.

<details>
<summary>Expand</summary>

### Polymorphic parameters

The same method accepts four forms — string, array, closure, object:

```php
$q->term('status', 'published'); // string
$q->term(['status' => 'published']); // array
$q->term(fn ($t) => $t->field('status')->value('published')); // closure
$q->term(Term::create('status', 'published')); // object
```

### OOP style

Each query type is a dedicated Node class supporting chaining:

```php
use ElasticKit\DSL\Query;
use ElasticKit\DSL\Queries\TermLevel\Term;
use ElasticKit\DSL\Queries\TermLevel\Range;
use ElasticKit\DSL\Queries\FullText\Match_;
use ElasticKit\DSL\Queries\Compound\Boolean;

$bool = Boolean::create()
->must(Match_::create('title', 'elasticsearch'))
->filter(Term::create('status', 'published')->boost(1.5));

// incremental build
if ($filterByPrice) {
$bool->filter(Range::create('price', [10, 100]));
}

$query = Query::create($bool);

$query->toArray(); // ['query' => ['bool' => [...]]]
$query->toJson(); // '{"query":{"bool":{...}}}'
```

### Compound query

```php
Expand All @@ -98,8 +70,7 @@ $results = ProductIndex::query()
'must' => fn ($q) => $q->match('title', 'elasticsearch'),
'filter' => fn ($q) => $q
->range('price', [10, 100])
->when($status, fn ($q) => $q->term('status', $status)) // conditional filter
->term('status', 'published'),
->when($status, fn ($q) => $q->term('status', $status)), // conditional filter
])
->highlight('title')
->sort('price', 'asc')
Expand All @@ -124,25 +95,29 @@ $results = ProductIndex::query()
}
```

### Clause appending (ClausesSupport)
### OOP style

The clauses of a `bool` query (must / should / filter / must_not) **append**, and accept the same four input forms as leaf queries:
Build each clause separately, then combine them into a query:

```php
// all four forms are equivalent, each produces one must clause
$q->bool(fn ($b) => $b->must(fn ($q) => $q->term('status', 'published')));
$q->bool(['must' => fn ($q) => $q->term('status', 'published')]);
$q->bool('must', fn ($q) => $q->term('status', 'published'));
use ElasticKit\DSL\Queries\Compound\Boolean;
use ElasticKit\DSL\Queries\FullText\Match_;
use ElasticKit\DSL\Queries\TermLevel\Range;
use ElasticKit\DSL\Queries\TermLevel\Term;

// clauses accumulate (multiple calls and list form both append)
$q->bool(fn ($b) => $b->must(...)->must(...)); // must: [q1, q2]
$q->bool(['must' => [$q1, $q2]]); // same
// Build the clauses
$status = Term::create('status', 'published')->boost(1.5);
$title = Match_::create('title', 'elasticsearch');

// contrast: minimum_should_match is a single-value property; later calls overwrite instead of append
$q->bool(fn ($b) => $b->minimumShouldMatch(1)->minimumShouldMatch(3)); // 3
```
// Combine into a bool query
$bool = Boolean::create()->must($title)->filter($status);
if ($filterByPrice) {
$bool->filter(Range::create('price', [10, 100]));
}

> `dis_max`, `span_or`, `span_near` and other array-clause containers behave the same way (queries / clauses append).
// Execute
$results = ProductIndex::query()->bool($bool)->size(20)->get();
```

### Aggregations

Expand All @@ -157,22 +132,25 @@ $results = ProductIndex::query()
$aggs = $results->aggregations();
```

### Nested query
### kNN search

```php
$results = ProductIndex::query()
->nested('comments', fn ($q) => $q->match('comments.body', 'great'))
->knn(fn ($k) => $k
->field('embedding')
->queryVector([0.12, 0.45, 0.78, /* ... */])
->numCandidates(100))
->size(10)
->get();
```

### Raw DSL pass-through
### Raw array

```php
// supports raw arrays with nested closures; query/aggs/parameters can be passed all at once
$query = Query::create([
'query' => [
'bool' => [
'must' => fn ($q) => $q->match('title', 'elasticsearch'),
'must' => fn ($q) => $q->match('title', 'elasticsearch'), // array may nest closures
'filter' => fn ($q) => $q->term('status', 'published'),
],
],
Expand All @@ -181,10 +159,34 @@ $query = Query::create([
]);
```

### Clause appending (ClausesSupport)

The clauses of a `bool` query (must / should / filter / must_not) **append** — repeated calls accumulate, they don't overwrite:

```php
$q->bool(fn ($b) => $b->must($q1)); // must: [q1]
$q->bool(fn ($b) => $b->must($q2)); // must: [q1, q2]
```

> `dis_max`, `span_or`, `span_near` and other array-clause containers behave the same way.

### Flexible arguments

The same method accepts multiple input forms — use whichever suits:

```php
$q->term('status', 'published'); // string
$q->term(['status' => 'published']); // array
$q->term(fn ($t) => $t->field('status')->value('published')); // closure
$q->term(Term::create('status', 'published')); // object
```

</details>

## Index Examples

Around the `Index` base class, dedicated classes cover routine index operations — pagination, CRUD, bulk writes, index management, zero-downtime rebuilds, and event hooks.

<details>
<summary>Expand</summary>

Expand Down
115 changes: 58 additions & 57 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@

[![Latest Version](https://img.shields.io/packagist/v/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit)
[![Total Downloads](https://img.shields.io/packagist/dt/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit)
[![Tests](https://github.com/ykan821/ElasticKit/actions/workflows/ci.yml/badge.svg)](https://github.com/ykan821/ElasticKit/actions/workflows/ci.yml)
[![PHP](https://img.shields.io/packagist/php-v/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit)
[![License](https://img.shields.io/packagist/l/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit)

PHP Elasticsearch DSL 查询构建库,覆盖查询、聚合、CRUD、批量写入、零停机重建。

## 集成

- **[ElasticKit Laravel](https://github.com/ykan821/ElasticKitLaravel)** — Laravel 集成(原生分页、artisan 重建)。`composer require ykan/elastickit-laravel`。

## 安装

```
Expand Down Expand Up @@ -50,46 +56,11 @@ $total = $results->total(); // 索引未设 $trackTotalHits = true 时为 null

## DSL 示例

ElasticKit 的 DSL 尽量贴近 ES 原生 API,以减少认知负担——熟悉 ES DSL 的人可以平滑迁移。每个查询类型都是一个专门的 `Node` 类,方法名与 ES 参数对应。

<details>
<summary>展开查看</summary>

### 多态参数

同一个方法支持字符串、数组、闭包、对象四种写法:

```php
$q->term('status', 'published'); // string
$q->term(['status' => 'published']); // array
$q->term(fn ($t) => $t->field('status')->value('published')); // closure
$q->term(Term::create('status', 'published')); // object
```

### OOP 风格

每个查询类型都是独立的 Node 类,支持链式调用:

```php
use ElasticKit\DSL\Query;
use ElasticKit\DSL\Queries\TermLevel\Term;
use ElasticKit\DSL\Queries\TermLevel\Range;
use ElasticKit\DSL\Queries\FullText\Match_;
use ElasticKit\DSL\Queries\Compound\Boolean;

$bool = Boolean::create()
->must(Match_::create('title', 'elasticsearch'))
->filter(Term::create('status', 'published')->boost(1.5));

// 增量构建
if ($filterByPrice) {
$bool->filter(Range::create('price', [10, 100]));
}

$query = Query::create($bool);

$query->toArray(); // ['query' => ['bool' => [...]]]
$query->toJson(); // '{"query":{"bool":{...}}}'
```

### 复合查询

```php
Expand All @@ -98,8 +69,7 @@ $results = ProductIndex::query()
'must' => fn ($q) => $q->match('title', 'elasticsearch'),
'filter' => fn ($q) => $q
->range('price', [10, 100])
->when($status, fn ($q) => $q->term('status', $status)) // 条件过滤
->term('status', 'published'),
->when($status, fn ($q) => $q->term('status', $status)), // 条件过滤
])
->highlight('title')
->sort('price', 'asc')
Expand All @@ -124,25 +94,29 @@ $results = ProductIndex::query()
}
```

### 子句追加(ClausesSupport)
### OOP 风格

`bool` 查询的子句(must / should / filter / must_not)**累加追加**,并接受与叶子查询相同的 4 种输入形式
先单独构造各个子句,再组合成查询

```php
// 4 种输入形式等价,都产出一条 must
$q->bool(fn ($b) => $b->must(fn ($q) => $q->term('status', 'published')));
$q->bool(['must' => fn ($q) => $q->term('status', 'published')]);
$q->bool('must', fn ($q) => $q->term('status', 'published'));
use ElasticKit\DSL\Queries\Compound\Boolean;
use ElasticKit\DSL\Queries\FullText\Match_;
use ElasticKit\DSL\Queries\TermLevel\Range;
use ElasticKit\DSL\Queries\TermLevel\Term;

// 子句累加(多次调用、列表形式都追加)
$q->bool(fn ($b) => $b->must(...)->must(...)); // must: [q1, q2]
$q->bool(['must' => [$q1, $q2]]); // 同上
// 构造子句
$status = Term::create('status', 'published')->boost(1.5);
$title = Match_::create('title', 'elasticsearch');

// 对比:minimum_should_match 是单值属性,后调覆盖而非追加
$q->bool(fn ($b) => $b->minimumShouldMatch(1)->minimumShouldMatch(3)); // 3
```
// 组合成 bool 查询
$bool = Boolean::create()->must($title)->filter($status);
if ($filterByPrice) {
$bool->filter(Range::create('price', [10, 100]));
}

> `dis_max`、`span_or`、`span_near` 等其他数组子句容器同理(queries / clauses 累加)。
// 执行
$results = ProductIndex::query()->bool($bool)->size(20)->get();
```

### 聚合

Expand All @@ -157,22 +131,25 @@ $results = ProductIndex::query()
$aggs = $results->aggregations();
```

### 嵌套查询
### kNN 搜索

```php
$results = ProductIndex::query()
->nested('comments', fn ($q) => $q->match('comments.body', 'great'))
->knn(fn ($k) => $k
->field('embedding')
->queryVector([0.12, 0.45, 0.78, /* ... */])
->numCandidates(100))
->size(10)
->get();
```

### 原生 DSL 透传
### 原生数组

```php
// 支持原生数组嵌套闭包,query/aggs/参数可一次性传入
$query = Query::create([
'query' => [
'bool' => [
'must' => fn ($q) => $q->match('title', 'elasticsearch'),
'must' => fn ($q) => $q->match('title', 'elasticsearch'), // 支持数组嵌套闭包
'filter' => fn ($q) => $q->term('status', 'published'),
],
],
Expand All @@ -181,10 +158,34 @@ $query = Query::create([
]);
```

### 子句追加(ClausesSupport)

`bool` 查询的子句(must / should / filter / must_not)**累加追加**——重复调用会累加,而非覆盖:

```php
$q->bool(fn ($b) => $b->must($q1)); // must: [q1]
$q->bool(fn ($b) => $b->must($q2)); // must: [q1, q2]
```

> `dis_max`、`span_or`、`span_near` 等其他数组子句容器同理。

### 灵活入参

同一个方法接受多种入参形式——按场景选用:

```php
$q->term('status', 'published'); // string
$q->term(['status' => 'published']); // array
$q->term(fn ($t) => $t->field('status')->value('published')); // closure
$q->term(Term::create('status', 'published')); // object
```

</details>

## Index 示例

围绕 `Index` 基类,ElasticKit 为索引日常操作封装了专用类——分页、CRUD、批量写入、索引管理、零停机重建、事件监听。

<details>
<summary>展开查看</summary>

Expand Down
1 change: 1 addition & 0 deletions src/DSL/Agg.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use ElasticKit\DSL\Aggs\Bucket;
use ElasticKit\DSL\Aggs\Metric;
use ElasticKit\DSL\Aggs\Pipeline;
use ElasticKit\DSL\Support\DeepClone;
use ElasticKit\DSL\Support\RegistersAgg;
use stdClass;

Expand Down
1 change: 1 addition & 0 deletions src/DSL/Node.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use ArgumentCountError;
use BadMethodCallException;
use Closure;
use ElasticKit\DSL\Support\DeepClone;
use InvalidArgumentException;
use LogicException;
use stdClass;
Expand Down
Loading
Loading