Lightweight Search Engine: Meilisearch
Table of Contents
Introduction⌗
Due to business requirements, I needed to implement a search feature for a project. After considering various options, I chose Meilisearch for the following reasons:
- Lightweight: The Docker image is only about 100MB, and it consumes minimal resources when running.
- Support for custom search fields: You can specify which fields to search and which fields to display in the results.
- Easy integration with Laravel: Laravel Scout provides convenient integration with Meilisearch.
Deployment⌗
First, configure the environment variables in the .env
file:
MEILISEARCH_HOST=http://localhost:7700
MEILISEARCH_KEY=masterKey
Then create a docker-compose.yml
file:
version: '3'
services:
meilisearch:
image: getmeili/meilisearch:v0.27.0
environment:
- MEILI_MASTER_KEY=masterKey
- MEILI_NO_ANALYTICS=true
ports:
- 7700:7700
volumes:
- ./data.ms:/data.ms
Start the service:
docker-compose up -d
Getting API Keys⌗
After starting the service, you need to get the API keys. You can use the following command:
curl \
-X GET 'http://localhost:7700/keys' \
-H 'Authorization: Bearer masterKey'
The response will be:
{
"private": {
"key": ""private": {
"key": "private_key_value",
"uid": "private_key_uid",
"name": "Default Private Key",
"description": "Use it for anything that requires authentication",
"actions": ["*"],
"indexes": ["*"],
"expiresAt": null,
"createdAt": "2022-04-30T10:00:00.000Z",
"updatedAt": "2022-04-30T10:00:00.000Z"
},
"public": {
"key": "public_key_value",
"uid": "public_key_uid",
"name": "Default Search Key",
"description": "Use it to search from the frontend",
"actions": ["search"],
"indexes": ["*"],
"expiresAt": null,
"createdAt": "2022-04-30T10:00:00.000Z",
"updatedAt": "2022-04-30T10:00:00.000Z"
}
}
The response contains two keys:
- Private key: Has full access to all operations
- Public key: Only has search permission, suitable for frontend use
Importing Data⌗
First, install the PHP SDK and Laravel Scout:
composer require meilisearch/meilisearch-php http-interop/http-factory-guzzle laravel/scout
Then, set up your model to use Meilisearch. For example, if you have a District
model:
<?php
namespace App\Models;
use Laravel\Scout\Searchable;
use Illuminate\Database\Eloquent\Model;
class District extends Model
{
use Searchable;
/**
* Get the indexable data array for the model.
*
* @return array
*/
public function toSearchableArray()
{
return [
'id' => $this->id,
'code' => $this->code,
'fullname' => $this->fullname,
'spelling' => $this->spelling,
];
}
}
After setting up the model, configure the config/scout.php
file:
# Meilisearch Engine
SCOUT_DRIVER=meilisearch
MEILISEARCH_KEY=
MEILISEARCH_HOST=http://localhost:7700
Batch Import⌗
Then run the following command to start importing data to Meilisearch:
# Import data from database to Meilisearch
php artisan scout:import "App\Models\District"
# Delete all data from the index
php artisan scout:flush "App\Models\District"
For more related features, please refer to the Laravel Scout official documentation.
Verifying Search Results⌗
$items = District::search('东京')->get();
I found that the query results couldn’t retrieve some Meilisearch highlight information. After checking the SQL log, I discovered that it requests the Meilisearch search API, and then uses the resulting ID list as a whereIn
condition to query the models from the database.
This means we would need to implement our own highlighting for matching words, but this creates a problem. For example, if I search for the keyword 东京
, PHP cannot replace 東京
in 東京都
because the characters don’t match exactly.
Directly Calling the RESTful API⌗
After trying Laravel Scout’s search functionality, I found it couldn’t meet highly customized requirements, so I had to handle the request logic manually.
curl --location --request POST 'http://localhost:7700/indexes/district/search' \
--header 'Authorization: Bearer masterKey' \
--header 'Content-Type: application/json' \
--data-raw '{"q":"东京","matches":true,"limit":10,"attributesToRetrieve":["code","fullname","spelling"],"attributesToHighlight":["fullname"]}'
Request body:
{
"q": "东京",
"limit": 2,
"matches": true,
"attributesToRetrieve": ["code", "fullname"],
"attributesToHighlight": ["fullname"]
}
- q: Search keyword
- limit: Limit the number of responses
- matches: Whether the response includes
_matchesInfo
- attributesToRetrieve: Fields to include in the response
- attributesToHighlight: Fields that need highlighting for frontend rendering
Response result:
{
"hits": [
{
"fullname": "東京都",
"code": "13",
"_formatted": {
"fullname": "<em>東京都</em>",
"code": "13"
},
"_matchesInfo": {
"fullname": [
{
"start": 0,
"length": 2
}
]
}
},
{
"fullname": "東京都千代田区",
"code": "13101",
"_formatted": {
"fullname": "<em>東京都</em>千代田区",
"code": "13101"
},
"_matchesInfo": {
"fullname": [
{
"start": 0,
"length": 2
}
]
}
}
],
"nbHits": 43,
"exhaustiveNbHits": false,
"query": "东京",
"limit": 2,
"offset": 0,
"processingTimeMs": 1
}
By returning this data to the frontend and having the frontend color the <em>
tags, highlighting can be implemented.
Index Settings⌗
Is that all? Not quite. In our business, we also wanted to sort by a specific field, but when I tried to use the sort
parameter in the search API, the results were always incorrect.
After carefully reading Meilisearch’s official documentation, I found that indexes can have the following settings:
- Displayed attributes: Settings for attribute visibility
- Distinct attribute: Field used for filtering when attributes are the same, see the official documentation example
- Filterable attributes: Settings for filterable fields, default is ‘’; For example, if there’s a status field in the data that needs filtering, this setting can be used
- Ranking rules: Settings for search engine weights, default is: “words”,“typo”,“proximity”,“attribute”,“sort”,“exactness”,“release_date:desc”
- Searchable attributes: Settings for searchable fields, default is ‘*’; For example, if some fields are used for sorting, like price, but shouldn’t be searchable, this setting can exclude the price field
- Sortable attributes: Settings for sortable fields, such as price
- Stop-words: Settings for non-searchable words, such as meaningless prepositions, pronouns, etc.
- Synonyms: Settings for synonyms
These settings can meet most business requirements. The response speed is indeed at the millisecond level, although the current data volume isn’t very large, so performance will need to be observed in the future.
Document Update Notes⌗
When performing UPDATE
operations on a model, be sure to SELECT
the fields defined in the model’s toSearchableArray
method in your query. Otherwise, the value updated to Meilisearch will be null
!
Dashboard⌗
I found it cumbersome to always use Postman or cURL when managing Meilisearch, and later discovered that Meilisearch comes with a simple Mini Dashboard.
Not available when
MEILI_ENV
is set toproduction
!
This doesn’t need to be packaged and deployed separately. When the Meilisearch service starts, you can access it directly through a browser, and you’ll see the following page:
Enter the API key from the Getting API Keys section, and you can view all indexes and test search functionality under each index.
Conclusion⌗
Meilisearch is indeed compact and powerful. If I encounter any issues in the future, I’ll continue to record and share them. Recently, I’ve seen a search engine developed in C++ called Typesense. Another tool to tinker with!
I hope this is helpful, Happy hacking…Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0Zy0