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:

  1. Lightweight: The Docker image is only about 100MB, and it consumes minimal resources when running.
  2. Support for custom search fields: You can specify which fields to search and which fields to display in the results.
  3. 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 to production!

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 your admin API key

Enter the API key from the Getting API Keys section, and you can view all indexes and test search functionality under each index.

Search keywords in meilisearch dashboard

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