Skip to content

Commit

Permalink
Merge pull request #9 from moitran/3-publish-books-from-master-data-m…
Browse files Browse the repository at this point in the history
…ysql-to-elasticsearch-and-switch-api-query-endpoint-to-elasticsearch

Publish MYSQL data to ES & searching books on ES
  • Loading branch information
moitran authored Jun 1, 2024
2 parents 3b49cf3 + 1d2f969 commit 77e2a04
Show file tree
Hide file tree
Showing 15 changed files with 428 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,8 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

VITE_APP_NAME="${APP_NAME}"

ELASTICSEARCH_HOST=elasticsearch
ELASTICSEARCH_PORT=9200
SCOUT_DRIVER=elastic
EXPLORER_ELASTIC_LOGGER_ENABLED=true
14 changes: 14 additions & 0 deletions app/ElasticSearch/ElasticSearchQueryBuilderFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace App\ElasticSearch;

use App\ElasticSearch\QueryBuilder\BookElasticQueryBuilder;
use App\ElasticSearch\QueryBuilder\ElasticQueryBuilderInterface;

class ElasticSearchQueryBuilderFactory
{
public function createBookElasticQueryBuilder(): ElasticQueryBuilderInterface
{
return new BookElasticQueryBuilder();
}
}
52 changes: 52 additions & 0 deletions app/ElasticSearch/QueryBuilder/BookElasticQueryBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace App\ElasticSearch\QueryBuilder;

use App\Http\Requests\Books\IndexRequest;
use App\Models\Book;
use Illuminate\Foundation\Http\FormRequest;
use InvalidArgumentException;
use JeroenG\Explorer\Domain\Query\QueryProperties\TrackTotalHits;
use JeroenG\Explorer\Domain\Syntax\Compound\BoolQuery;
use JeroenG\Explorer\Domain\Syntax\Matching;
use JeroenG\Explorer\Domain\Syntax\Nested;
use JeroenG\Explorer\Domain\Syntax\Term;
use JeroenG\Explorer\Infrastructure\Scout\Builder;

class BookElasticQueryBuilder implements ElasticQueryBuilderInterface
{
public function build(FormRequest $request): Builder
{
if (! $request instanceof IndexRequest) {
throw new InvalidArgumentException('Request must be an instance of IndexRequest');
}

$query = $request->query('query');
$categoryId = $request->query('category_id');
$providerId = $request->query('provider_id');

/** @var Builder $builder */
$builder = Book::search();

if ($query) {
$bool = new BoolQuery();
// title match query OR author match query OR book_number match query
$bool->should(new Matching('title', $query));
$bool->should(new Matching('author', $query));
$bool->should(new Matching('book_number', $query));
$builder->filter($bool);
}

if ($categoryId) {
$builder->must(new Nested('category', new Term('category.id', $categoryId)));
}

if ($providerId) {
$builder->must(new Nested('provider', new Term('provider.id', $providerId)));
}

$builder->property(TrackTotalHits::all());

return $builder;
}
}
11 changes: 11 additions & 0 deletions app/ElasticSearch/QueryBuilder/ElasticQueryBuilderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace App\ElasticSearch\QueryBuilder;

use Illuminate\Foundation\Http\FormRequest;
use JeroenG\Explorer\Infrastructure\Scout\Builder;

interface ElasticQueryBuilderInterface
{
public function build(FormRequest $request): Builder;
}
99 changes: 99 additions & 0 deletions app/Http/Controllers/BookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ public function __construct(private readonly BookService $bookService)
* ),
*
* @OA\Parameter(
* name="page",
* in="query",
* description="Number of page",
* required=false,
*
* @OA\Schema(type="integer", default=1)
* ),
*
* @OA\Parameter(
* name="query",
* in="query",
* description="Search query",
Expand Down Expand Up @@ -90,6 +99,96 @@ public function index(IndexRequest $request): JsonResource
return BookResource::collection($books);
}

/**
* Display a listing of the resource.
*
* @OA\Get(
* path="/api/books/search",
* summary="Get a list of books",
* tags={"Books"},
*
* @OA\Parameter(
* name="per_page",
* in="query",
* description="Number of items per page",
* required=false,
*
* @OA\Schema(type="integer", default=10)
* ),
*
* @OA\Parameter(
* name="page",
* in="query",
* description="Number of page",
* required=false,
*
* @OA\Schema(type="integer", default=1)
* ),
*
* @OA\Parameter(
* name="query",
* in="query",
* description="Search query",
* required=false,
*
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="category_id",
* in="query",
* description="Search by Category",
* required=false,
*
* @OA\Schema(type="string", format="uuid")
* ),
*
* @OA\Parameter(
* name="provider_id",
* in="query",
* description="Search by Provider",
* required=false,
*
* @OA\Schema(type="string", format="uuid")
* ),
*
* @OA\Parameter(
* name="order_by",
* in="query",
* description="Field to order by",
* required=false,
*
* @OA\Schema(type="string", enum={"title", "author", "created_at", "updated_at"}, default="created_at")
* ),
*
* @OA\Parameter(
* name="order_type",
* in="query",
* description="Order type (ascending or descending)",
* required=false,
*
* @OA\Schema(type="string", enum={"asc", "desc"}, default="desc")
* ),
*
* @OA\Response(
* response=200,
* description="Successful operation",
*
* @OA\JsonContent(
* type="array",
*
* @OA\Items(ref="#/components/schemas/BookResource")
* )
* )
* )
*/
public function search(IndexRequest $indexRequest): JsonResource
{
$books = $this->bookService->search($indexRequest);

return BookResource::collection($books);
}

/**
* Show the form for creating a new resource.
*/
Expand Down
4 changes: 3 additions & 1 deletion app/Http/Requests/Books/IndexRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ public function rules(): array
return [
'query' => 'nullable|string|max:255',
'per_page' => 'nullable|integer|min:1',
'page' => 'nullable|integer|min:1|max:100',
'page' => 'nullable|integer|min:1',
'order_by' => 'nullable|string|in:title,author,created_at,updated_at',
'order_type' => 'nullable|string|in:asc,desc',
'category_id' => 'nullable|exists:categories,id',
'provider_id' => 'nullable|exists:providers,id',
];
}
}
65 changes: 48 additions & 17 deletions app/Models/Book.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
namespace App\Models;

use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use JeroenG\Explorer\Application\Explored;
use JeroenG\Explorer\Application\IndexSettings;
use JeroenG\Explorer\Domain\Analysis\Analysis;
use JeroenG\Explorer\Domain\Analysis\Analyzer\StandardAnalyzer;
use JeroenG\Explorer\Domain\Analysis\Filter\SynonymFilter;
use Laravel\Scout\Searchable;

/**
Expand Down Expand Up @@ -62,6 +66,13 @@ class Book extends Model implements Explored, IndexSettings
'author',
];

public static array $sortMapping = [
'title' => 'slug',
'author' => 'author',
'created_at' => 'created_at',
'updated_at' => 'updated_at',
];

public function sluggable(): array
{
return [
Expand All @@ -73,31 +84,38 @@ public function sluggable(): array

public function indexSettings(): array
{
return [
'analysis' => [
'analyzer' => [
'standard_lowercase' => [
'type' => 'custom',
'tokenizer' => 'standard',
'filter' => ['lowercase'],
],
],
],
];
$synonymFilter = new SynonymFilter();

$synonymAnalyzer = new StandardAnalyzer('synonym');
$synonymAnalyzer->setFilters(['lowercase', $synonymFilter]);

return (new Analysis())
->addAnalyzer($synonymAnalyzer)
->addFilter($synonymFilter)
->build();
}

public function mappableAs(): array
{
return [
'id' => 'keyword',
'book_number' => 'keyword',
'slug' => 'text',
'title' => 'text',
'slug' => 'keyword',
'title' => [
'type' => 'text',
'analyzer' => 'synonym',
],
'author' => 'text',
'category.name' => 'text',
'category.slug' => 'text',
'created_at' => 'date',
'updated_at' => 'date',
'category' => [
'id' => 'keyword',
'name' => 'text',
],
'provider' => [
'id' => 'keyword',
'name' => 'text',
],
];
}

Expand All @@ -109,10 +127,18 @@ public function toSearchableArray(): array
'slug' => $this->slug,
'title' => $this->title,
'author' => $this->author,
'category.name' => $this->category->name,
'category.slug' => $this->category->slug,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
// Category data
'category' => [
'id' => $this->category->id,
'name' => $this->category->name,
],
// Provider data
'provider' => [
'id' => $this->provider->id,
'name' => $this->provider->name,
],
];
}

Expand All @@ -131,4 +157,9 @@ public function provider()
{
return $this->belongsTo(Provider::class);
}

protected function makeAllSearchableUsing(Builder $query): Builder
{
return $query->with(['category', 'provider']);
}
}
19 changes: 19 additions & 0 deletions app/Services/BookService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

namespace App\Services;

use App\ElasticSearch\ElasticSearchQueryBuilderFactory;
use App\Exceptions\BookNotFoundException;
use App\Http\Requests\Books\IndexRequest;
use App\Models\Book;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;

class BookService
{
public function __construct(private readonly ElasticSearchQueryBuilderFactory $elasticSearchQueryBuilderFactory)
{
}

public function getAllBooks(int $perPage = 10, ?string $query = null, string $orderBy = 'created_at', string $orderType = 'desc'): LengthAwarePaginator
{
$books = Book::orderBy($orderBy, $orderType);
Expand All @@ -23,6 +29,19 @@ public function getAllBooks(int $perPage = 10, ?string $query = null, string $or
return $books->paginate($perPage);
}

public function search(IndexRequest $indexRequest): LengthAwarePaginator
{
$perPage = $indexRequest->integer('per_page', 10);
$orderBy = $indexRequest->query('order_by', 'created_at');
$orderType = $indexRequest->query('order_type', 'desc');

$bookElasticQueryBuilder = $this->elasticSearchQueryBuilderFactory->createBookElasticQueryBuilder();

return $bookElasticQueryBuilder->build($indexRequest)
->orderBy(Book::$sortMapping[$orderBy], $orderType)
->paginate($perPage);
}

public function getBookById(string $id): Book
{
$book = Book::find($id);
Expand Down
2 changes: 1 addition & 1 deletion config/explorer.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@
* to a PSR-3 logger. Disabled by default for performance.
*/
'logging' => env('EXPLORER_ELASTIC_LOGGER_ENABLED', false),
'logger' => null,
'logger' => 'elasticsearch',
];
6 changes: 6 additions & 0 deletions config/logging.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@
'path' => storage_path('logs/laravel.log'),
],

'elasticsearch' => [
'driver' => 'daily',
'path' => storage_path('logs/elasticsearch.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
],
],

];
2 changes: 1 addition & 1 deletion config/scout.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
|
*/

'driver' => env('SCOUT_DRIVER', 'algolia'),
'driver' => env('SCOUT_DRIVER', 'elastic'),

/*
|--------------------------------------------------------------------------
Expand Down
Loading

0 comments on commit 77e2a04

Please sign in to comment.