์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ” PHP์™€ Elasticsearch๋กœ ๋งŒ๋“œ๋Š” ๊ฐ•๋ ฅํ•œ ์ „๋ฌธ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿ” PHP์™€ Elasticsearch๋กœ ๋งŒ๋“œ๋Š” ๊ฐ•๋ ฅํ•œ ์ „๋ฌธ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ํ•œ ๋‹จ๊ณ„ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด? ์ง€๊ธˆ ๋ฐ”๋กœ ์‹œ์ž‘ํ•ด๋ณด์ž!

๐ŸŽฏ ์™œ Elasticsearch์ธ๊ฐ€?

์›น์‚ฌ์ดํŠธ๋‚˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค๋‹ค ๋ณด๋ฉด ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์€ ํ•„์ˆ˜์•ผ. ๊ทธ๋Ÿฐ๋ฐ MySQL์˜ LIKE ์ฟผ๋ฆฌ๋กœ๋Š” ํ•œ๊ณ„๊ฐ€ ๋ช…ํ™•ํ•˜์ง€. ์‚ฌ์šฉ์ž๊ฐ€ ๋Š˜์–ด๋‚˜๊ณ  ๋ฐ์ดํ„ฐ๊ฐ€ ์Œ“์ด๋ฉด ๊ฒ€์ƒ‰ ์†๋„๊ฐ€ ๋А๋ ค์ง€๊ณ , ์˜คํƒ€๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๊ฒฐ๊ณผ๊ฐ€ ์•ˆ ๋‚˜์˜ค๊ณ , ๊ด€๋ จ์„ฑ ๋†’์€ ๊ฒฐ๊ณผ๋ฅผ ์šฐ์„ ์ ์œผ๋กœ ๋ณด์—ฌ์ฃผ๊ธฐ๋„ ์–ด๋ ต์ง€. ๐Ÿ˜“

์ด๋Ÿด ๋•Œ ๋“ฑ์žฅํ•˜๋Š” ๊ฒŒ ๋ฐ”๋กœ Elasticsearch์•ผ! ์ด๊ฑด ๋‹จ์ˆœํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์•„๋‹ˆ๋ผ ๊ฒ€์ƒ‰์— ํŠนํ™”๋œ ์—”์ง„์ด๊ฑฐ๋“ . ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋น ๋ฅด๊ฒŒ ๊ฒ€์ƒ‰ํ•˜๊ณ , ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ๋„ ๊ฐ€๋Šฅํ•˜๊ณ , ์‹ฌ์ง€์–ด ์˜คํƒ€๋„ ์–ด๋А ์ •๋„ ์ปค๋ฒ„ํ•ด์ค˜. ๋„ค์ด๋ฒ„, ๊ตฌ๊ธ€ ๊ฐ™์€ ๋Œ€ํ˜• ํฌํ„ธ๋ถ€ํ„ฐ ์ž‘์€ ์Šคํƒ€ํŠธ์—…๊นŒ์ง€ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ๊ฐ€ ๋‹ค ์žˆ์–ด.

๐Ÿ’ก ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ํ™œ์šฉ ๊ฐ€๋Šฅ!

์žฌ๋Šฅ๋„ท์ฒ˜๋Ÿผ ๋‹ค์–‘ํ•œ ์žฌ๋Šฅ๊ณผ ์„œ๋น„์Šค๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ํ”Œ๋žซํผ์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์žฌ๋Šฅ์„ ๋น ๋ฅด๊ณ  ์ •ํ™•ํ•˜๊ฒŒ ์ฐพ๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด์•ผ. Elasticsearch๋ฅผ ํ™œ์šฉํ•˜๋ฉด "๋””์ž์ธ", "๋””์ž์ด๋„ˆ", "๋””์ž์ธ ์ž‘์—…" ๊ฐ™์€ ์œ ์‚ฌ ๊ฒ€์ƒ‰์–ด๋„ ๋˜‘๋˜‘ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์ง€!

์ „ํ†ต์  ๊ฒ€์ƒ‰ vs Elasticsearch MySQL LIKE ๊ฒ€์ƒ‰ SELECT * FROM products WHERE name LIKE '%๊ฒ€์ƒ‰์–ด%' ๋ฐ์ดํ„ฐ ์ฆ๊ฐ€ ์‹œ ์†๋„ ์ €ํ•˜ ์˜คํƒ€ ์ฒ˜๋ฆฌ ๋ถˆ๊ฐ€ ๊ด€๋ จ์„ฑ ์ˆœ์œ„ ์–ด๋ ค์›€ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ ํ•„์š” ์ „๋ฌธ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ œํ•œ Elasticsearch ์—ญ์ƒ‰์ธ ๊ธฐ๋ฐ˜ ๊ณ ์† ๊ฒ€์ƒ‰ RESTful API ์ œ๊ณต ๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„ ์‘๋‹ต์†๋„ ํผ์ง€ ๊ฒ€์ƒ‰์œผ๋กœ ์˜คํƒ€ ์ฒ˜๋ฆฌ ์Šค์ฝ”์–ด๋ง ๊ธฐ๋ฐ˜ ์ •๋ ฌ ์ง๊ด€์ ์ธ ์ฟผ๋ฆฌ DSL ํ˜•ํƒœ์†Œ ๋ถ„์„ ์ง€์›

๐Ÿ—๏ธ Elasticsearch ๊ธฐ๋ณธ ๊ฐœ๋… ์ดํ•ดํ•˜๊ธฐ

๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์ „์— Elasticsearch์˜ ๊ธฐ๋ณธ ๊ฐœ๋…์„ ์•Œ์•„์•ผ ํ•ด. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ๋น„์Šทํ•˜์ง€๋งŒ ์šฉ์–ด๊ฐ€ ์ข€ ๋‹ฌ๋ผ์„œ ์ฒ˜์Œ์—” ํ—ท๊ฐˆ๋ฆด ์ˆ˜ ์žˆ๊ฑฐ๋“ .

๐Ÿ“ฆ Index (์ธ๋ฑ์Šค)

MySQL์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ๋น„์Šทํ•œ ๊ฐœ๋…์ด์•ผ. ๊ด€๋ จ๋œ ๋ฌธ์„œ๋“ค์„ ๋ชจ์•„๋†“์€ ์ปฌ๋ ‰์…˜์ด์ง€. ์˜ˆ๋ฅผ ๋“ค์–ด 'products', 'users', 'posts' ๊ฐ™์€ ์ธ๋ฑ์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด.

๐Ÿ“„ Document (๋ฌธ์„œ)

MySQL์˜ row์™€ ๊ฐ™์€ ๊ฐœ๋…. JSON ํ˜•ํƒœ๋กœ ์ €์žฅ๋˜๋ฉฐ, ์‹ค์ œ ๊ฒ€์ƒ‰ ๋Œ€์ƒ์ด ๋˜๋Š” ๋ฐ์ดํ„ฐ์•ผ. ๊ฐ ๋ฌธ์„œ๋Š” ๊ณ ์œ ํ•œ ID๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์ง€.

๐Ÿท๏ธ Field (ํ•„๋“œ)

MySQL์˜ column๊ณผ ๊ฐ™์•„. ๋ฌธ์„œ ๋‚ด์˜ ๊ฐ ์†์„ฑ์„ ์˜๋ฏธํ•ด. 'title', 'content', 'price' ๊ฐ™์€ ํ•„๋“œ๋“ค์ด ์žˆ์„ ์ˆ˜ ์žˆ์ง€.

๐Ÿ” Mapping (๋งคํ•‘)

MySQL์˜ ์Šคํ‚ค๋งˆ์™€ ๋น„์Šทํ•ด. ๊ฐ ํ•„๋“œ์˜ ๋ฐ์ดํ„ฐ ํƒ€์ž…๊ณผ ๋ถ„์„ ๋ฐฉ๋ฒ•์„ ์ •์˜ํ•˜๋Š” ๊ฑฐ์•ผ. ๊ฒ€์ƒ‰ ํ’ˆ์งˆ์— ํฐ ์˜ํ–ฅ์„ ๋ฏธ์น˜๋Š” ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์ง€.

๐ŸŽ“ ์—ญ์ƒ‰์ธ(Inverted Index)์ด๋ž€?

Elasticsearch๊ฐ€ ๋น ๋ฅธ ์ด์œ ๋Š” ์—ญ์ƒ‰์ธ ๊ตฌ์กฐ ๋•๋ถ„์ด์•ผ. ์ผ๋ฐ˜์ ์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” "๋ฌธ์„œ โ†’ ๋‹จ์–ด" ๋ฐฉํ–ฅ์œผ๋กœ ์ €์žฅํ•˜์ง€๋งŒ, Elasticsearch๋Š” "๋‹จ์–ด โ†’ ๋ฌธ์„œ" ๋ฐฉํ–ฅ์œผ๋กœ ์ €์žฅํ•ด.

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ:

๋ฌธ์„œ 1: "PHP๋Š” ์›น ๊ฐœ๋ฐœ์— ์ข‹์€ ์–ธ์–ด์ž…๋‹ˆ๋‹ค"
๋ฌธ์„œ 2: "PHP์™€ Elasticsearch๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ์ข‹์Šต๋‹ˆ๋‹ค"

์—ญ์ƒ‰์ธ ๊ตฌ์กฐ:

โ€ข PHP โ†’ [๋ฌธ์„œ1, ๋ฌธ์„œ2]
โ€ข ์›น โ†’ [๋ฌธ์„œ1]
โ€ข ๊ฐœ๋ฐœ โ†’ [๋ฌธ์„œ1]
โ€ข Elasticsearch โ†’ [๋ฌธ์„œ2]
โ€ข ์ข‹์€, ์ข‹์Šต๋‹ˆ๋‹ค โ†’ [๋ฌธ์„œ1, ๋ฌธ์„œ2]

์ด๋ ‡๊ฒŒ ์ €์žฅํ•˜๋ฉด "PHP"๋ฅผ ๊ฒ€์ƒ‰ํ•  ๋•Œ ๋ชจ๋“  ๋ฌธ์„œ๋ฅผ ๋’ค์งˆ ํ•„์š” ์—†์ด ๋ฐ”๋กœ ๊ด€๋ จ ๋ฌธ์„œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด! ๐Ÿš€

โš™๏ธ ํ™˜๊ฒฝ ์„ค์ •: PHP์™€ Elasticsearch ์—ฐ๊ฒฐํ•˜๊ธฐ

์ž, ์ด์ œ ์‹ค์ „์œผ๋กœ ๋“ค์–ด๊ฐ€๋ณด์ž! PHP์—์„œ Elasticsearch๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๊ณต์‹ ํด๋ผ์ด์–ธํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•ด์•ผ ํ•ด.

Step 1: Elasticsearch ์„ค์น˜

๋จผ์ € Elasticsearch๋ฅผ ์„ค์น˜ํ•ด์•ผ ํ•ด. Docker๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ •๋ง ๊ฐ„๋‹จํ•ด:

docker pull docker.elastic.co/elasticsearch/elasticsearch:8.11.0

docker run -d --name elasticsearch \
  -p 9200:9200 -p 9300:9300 \
  -e "discovery.type=single-node" \
  -e "xpack.security.enabled=false" \
  docker.elastic.co/elasticsearch/elasticsearch:8.11.0

์„ค์น˜๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:9200์— ์ ‘์†ํ•ด๋ด. JSON ํ˜•ํƒœ์˜ ์‘๋‹ต์ด ๋‚˜์˜ค๋ฉด ์„ฑ๊ณต์ด์•ผ! ๐ŸŽ‰

Step 2: PHP Elasticsearch ํด๋ผ์ด์–ธํŠธ ์„ค์น˜

Composer๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ณต์‹ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์„ค์น˜ํ•˜์ž:

composer require elasticsearch/elasticsearch

์„ค์น˜๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด PHP ์ฝ”๋“œ์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด!

Step 3: ๊ธฐ๋ณธ ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ

์ œ๋Œ€๋กœ ์—ฐ๊ฒฐ๋˜๋Š”์ง€ ํ™•์ธํ•ด๋ณด์ž:

<?php
require 'vendor/autoload.php';

use Elastic\Elasticsearch\ClientBuilder;

// Elasticsearch ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ
$client = ClientBuilder::create()
    ->setHosts(['localhost:9200'])
    ->build();

// ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ
try {
    $response = $client->info();
    echo "์—ฐ๊ฒฐ ์„ฑ๊ณต!\n";
    print_r($response->asArray());
} catch (Exception $e) {
    echo "์—ฐ๊ฒฐ ์‹คํŒจ: " . $e->getMessage();
}

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ

Elasticsearch 8.x ๋ฒ„์ „๋ถ€ํ„ฐ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ณด์•ˆ ๊ธฐ๋Šฅ์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์–ด. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” xpack.security.enabled=false๋กœ ๋น„ํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ ์ธ์ฆ์„ ์„ค์ •ํ•ด์•ผ ํ•ด!

๐Ÿ“ ์ธ๋ฑ์Šค ์ƒ์„ฑ๊ณผ ๋งคํ•‘ ์„ค์ •

์ด์ œ ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ์ธ๋ฑ์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. ์˜ˆ๋ฅผ ๋“ค์–ด ์ƒํ’ˆ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณผ๊ฒŒ.

๐ŸŽจ ๋งคํ•‘ ์„ค๊ณ„์˜ ์ค‘์š”์„ฑ

๋งคํ•‘์€ ๊ฒ€์ƒ‰ ํ’ˆ์งˆ์„ ๊ฒฐ์ •ํ•˜๋Š” ํ•ต์‹ฌ์ด์•ผ. ๊ฐ ํ•„๋“œ๋ฅผ ์–ด๋–ป๊ฒŒ ๋ถ„์„ํ• ์ง€, ์–ด๋–ค ํƒ€์ž…์œผ๋กœ ์ €์žฅํ• ์ง€ ์ •์˜ํ•˜๋Š” ๊ฑฐ๊ฑฐ๋“ . ํ•œ ๋ฒˆ ์„ค์ •ํ•˜๋ฉด ๋ณ€๊ฒฝ์ด ์–ด๋ ค์šฐ๋‹ˆ๊นŒ ์ฒ˜์Œ๋ถ€ํ„ฐ ์‹ ์ค‘ํ•˜๊ฒŒ ์„ค๊ณ„ํ•ด์•ผ ํ•ด!

<?php
require 'vendor/autoload.php';

use Elastic\Elasticsearch\ClientBuilder;

$client = ClientBuilder::create()
    ->setHosts(['localhost:9200'])
    ->build();

// ์ธ๋ฑ์Šค ์ƒ์„ฑ ๋ฐ ๋งคํ•‘ ์„ค์ •
$params = [
    'index' => 'products',
    'body' => [
        'settings' => [
            'number_of_shards' => 1,
            'number_of_replicas' => 0,
            'analysis' => [
                'analyzer' => [
                    'korean' => [
                        'type' => 'custom',
                        'tokenizer' => 'nori_tokenizer',
                        'filter' => ['lowercase', 'nori_part_of_speech']
                    ]
                ]
            ]
        ],
        'mappings' => [
            'properties' => [
                'id' => [
                    'type' => 'integer'
                ],
                'name' => [
                    'type' => 'text',
                    'analyzer' => 'korean',
                    'fields' => [
                        'keyword' => [
                            'type' => 'keyword'
                        ]
                    ]
                ],
                'description' => [
                    'type' => 'text',
                    'analyzer' => 'korean'
                ],
                'category' => [
                    'type' => 'keyword'
                ],
                'price' => [
                    'type' => 'integer'
                ],
                'tags' => [
                    'type' => 'keyword'
                ],
                'created_at' => [
                    'type' => 'date',
                    'format' => 'yyyy-MM-dd HH:mm:ss'
                ],
                'rating' => [
                    'type' => 'float'
                ],
                'stock' => [
                    'type' => 'integer'
                ]
            ]
        ]
    ]
];

try {
    // ๊ธฐ์กด ์ธ๋ฑ์Šค๊ฐ€ ์žˆ์œผ๋ฉด ์‚ญ์ œ
    if ($client->indices()->exists(['index' => 'products'])->asBool()) {
        $client->indices()->delete(['index' => 'products']);
    }
    
    // ์ƒˆ ์ธ๋ฑ์Šค ์ƒ์„ฑ
    $response = $client->indices()->create($params);
    echo "์ธ๋ฑ์Šค ์ƒ์„ฑ ์™„๋ฃŒ!\n";
} catch (Exception $e) {
    echo "์—๋Ÿฌ ๋ฐœ์ƒ: " . $e->getMessage();
}

๐Ÿ”‘ ๋งคํ•‘ ์„ค์ • ํ•ด์„ค

1. text vs keyword

โ€ข text: ์ „๋ฌธ ๊ฒ€์ƒ‰์šฉ. ํ˜•ํƒœ์†Œ ๋ถ„์„์„ ๊ฑฐ์ณ ํ† ํฐ์œผ๋กœ ๋‚˜๋‰˜์–ด ์ €์žฅ๋ผ. "์Šค๋งˆํŠธํฐ ์ผ€์ด์Šค"๋Š” "์Šค๋งˆํŠธํฐ", "์ผ€์ด์Šค"๋กœ ๋ถ„๋ฆฌ๋˜์ง€.
โ€ข keyword: ์ •ํ™•ํ•œ ๋งค์นญ์šฉ. ๋ถ„์„ ์—†์ด ์›๋ณธ ๊ทธ๋Œ€๋กœ ์ €์žฅ๋ผ. ์นดํ…Œ๊ณ ๋ฆฌ, ํƒœ๊ทธ, ์ƒํƒœ๊ฐ’ ๊ฐ™์€ ๊ณณ์— ์‚ฌ์šฉํ•ด.

2. multi-field (fields)

name ํ•„๋“œ๋ฅผ ๋ณด๋ฉด text์™€ keyword ๋‘ ๊ฐ€์ง€๋กœ ์ €์žฅํ•˜๊ณ  ์žˆ์–ด. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ „๋ฌธ ๊ฒ€์ƒ‰๋„ ํ•˜๊ณ , ์ •๋ ฌ์ด๋‚˜ ์ง‘๊ณ„๋„ ํ•  ์ˆ˜ ์žˆ์ง€!

3. analyzer

ํ•œ๊ตญ์–ด ๊ฒ€์ƒ‰์„ ์œ„ํ•ด nori ๋ถ„์„๊ธฐ๋ฅผ ์‚ฌ์šฉํ–ˆ์–ด. ์ด๊ฑด Elasticsearch์— ๊ธฐ๋ณธ ํฌํ•จ๋œ ํ•œ๊ตญ์–ด ํ˜•ํƒœ์†Œ ๋ถ„์„๊ธฐ์•ผ. "๋จน์—ˆ์Šต๋‹ˆ๋‹ค"๋ฅผ "๋จน๋‹ค"๋กœ ์ •๊ทœํ™”ํ•ด์ฃผ๋Š” ๋˜‘๋˜‘ํ•œ ๋…€์„์ด์ง€! ๐Ÿง 

๋ฐ์ดํ„ฐ ํƒ€์ž…๋ณ„ ํ™œ์šฉ ๋ฐฉ๋ฒ• Text ํƒ€์ž… ์ „๋ฌธ ๊ฒ€์ƒ‰์— ์ตœ์ ํ™” ํ˜•ํƒœ์†Œ ๋ถ„์„ ์ˆ˜ํ–‰ ๋ถ€๋ถ„ ๋งค์นญ ๊ฐ€๋Šฅ ๊ด€๋ จ์„ฑ ์Šค์ฝ”์–ด๋ง ์ •๋ ฌ/์ง‘๊ณ„ ๋ถˆ๊ฐ€ ์‚ฌ์šฉ์ฒ˜: ์ œ๋ชฉ, ๋ณธ๋ฌธ, ์„ค๋ช… Keyword ํƒ€์ž… ์ •ํ™•ํ•œ ๋งค์นญ์— ์ตœ์ ํ™” ๋ถ„์„ ์—†์ด ์›๋ณธ ์ €์žฅ ์™„์ „ ์ผ์น˜ ๊ฒ€์ƒ‰ ์ •๋ ฌ/์ง‘๊ณ„ ๊ฐ€๋Šฅ ํ•„ํ„ฐ๋ง์— ํšจ์œจ์  ์‚ฌ์šฉ์ฒ˜: ์นดํ…Œ๊ณ ๋ฆฌ, ํƒœ๊ทธ, ID ์ˆซ์ž/๋‚ ์งœ ํƒ€์ž… ๋ฒ”์œ„ ๊ฒ€์ƒ‰์— ์ตœ์ ํ™” ๋ฒ”์œ„ ์ฟผ๋ฆฌ ์ง€์› ์ •๋ ฌ ๊ฐ€๋Šฅ ์ง‘๊ณ„ ์—ฐ์‚ฐ ๊ฐ€๋Šฅ ํ†ต๊ณ„ ๋ถ„์„ ๊ฐ€๋Šฅ ์‚ฌ์šฉ์ฒ˜: ๊ฐ€๊ฒฉ, ๋‚ ์งœ, ํ‰์ 

๐Ÿ’พ ๋ฌธ์„œ ์ƒ‰์ธํ•˜๊ธฐ (Indexing)

์ธ๋ฑ์Šค๋ฅผ ๋งŒ๋“ค์—ˆ์œผ๋‹ˆ ์ด์ œ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ์–ด๋ณด์ž! ๋ฌธ์„œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์—ฌ๋Ÿฌ ๊ฐ€์ง€๊ฐ€ ์žˆ์–ด.

๋‹จ์ผ ๋ฌธ์„œ ์ƒ‰์ธ

<?php
// ๋‹จ์ผ ์ƒํ’ˆ ์ถ”๊ฐ€
$params = [
    'index' => 'products',
    'id' => 1,
    'body' => [
        'id' => 1,
        'name' => '๋ฌด์„  ๋ธ”๋ฃจํˆฌ์Šค ์ด์–ดํฐ',
        'description' => '๊ณ ์Œ์งˆ ๋ฌด์„  ์ด์–ดํฐ์œผ๋กœ ์ตœ๋Œ€ 24์‹œ๊ฐ„ ์žฌ์ƒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋…ธ์ด์ฆˆ ์บ”์Šฌ๋ง ๊ธฐ๋Šฅ ์ง€์›',
        'category' => '์ „์ž์ œํ’ˆ',
        'price' => 89000,
        'tags' => ['์ด์–ดํฐ', '๋ธ”๋ฃจํˆฌ์Šค', '๋ฌด์„ ', '๋…ธ์ด์ฆˆ์บ”์Šฌ๋ง'],
        'created_at' => date('Y-m-d H:i:s'),
        'rating' => 4.5,
        'stock' => 150
    ]
];

try {
    $response = $client->index($params);
    echo "๋ฌธ์„œ ์ถ”๊ฐ€ ์™„๋ฃŒ! ID: " . $response['_id'] . "\n";
} catch (Exception $e) {
    echo "์—๋Ÿฌ: " . $e->getMessage();
}

๋Œ€๋Ÿ‰ ๋ฌธ์„œ ์ƒ‰์ธ (Bulk API)

์—ฌ๋Ÿฌ ๋ฌธ์„œ๋ฅผ ํ•œ ๋ฒˆ์— ์ถ”๊ฐ€ํ•  ๋•Œ๋Š” Bulk API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ›จ์”ฌ ๋นจ๋ผ! ๋„คํŠธ์›Œํฌ ์™•๋ณต ํšŸ์ˆ˜๊ฐ€ ์ค„์–ด๋“ค๊ฑฐ๋“ .

<?php
// ์—ฌ๋Ÿฌ ์ƒํ’ˆ ํ•œ ๋ฒˆ์— ์ถ”๊ฐ€
$products = [
    [
        'id' => 2,
        'name' => '์Šค๋งˆํŠธ์›Œ์น˜ ํ”„๋กœ',
        'description' => '๊ฑด๊ฐ• ๊ด€๋ฆฌ์™€ ์šด๋™ ์ถ”์ ์ด ๊ฐ€๋Šฅํ•œ ์Šค๋งˆํŠธ์›Œ์น˜',
        'category' => '์ „์ž์ œํ’ˆ',
        'price' => 350000,
        'tags' => ['์Šค๋งˆํŠธ์›Œ์น˜', 'ํ—ฌ์Šค์ผ€์–ด', '์šด๋™'],
        'created_at' => date('Y-m-d H:i:s'),
        'rating' => 4.7,
        'stock' => 80
    ],
    [
        'id' => 3,
        'name' => '๊ฒŒ์ด๋ฐ ํ‚ค๋ณด๋“œ',
        'description' => '๊ธฐ๊ณ„์‹ ์Šค์œ„์น˜๋ฅผ ์‚ฌ์šฉํ•œ RGB ๊ฒŒ์ด๋ฐ ํ‚ค๋ณด๋“œ',
        'category' => '์ปดํ“จํ„ฐ์ฃผ๋ณ€๊ธฐ๊ธฐ',
        'price' => 129000,
        'tags' => ['ํ‚ค๋ณด๋“œ', '๊ฒŒ์ด๋ฐ', '๊ธฐ๊ณ„์‹', 'RGB'],
        'created_at' => date('Y-m-d H:i:s'),
        'rating' => 4.3,
        'stock' => 200
    ],
    [
        'id' => 4,
        'name' => '๋ฌด์„  ๋งˆ์šฐ์Šค',
        'description' => '์ธ์ฒด๊ณตํ•™์  ๋””์ž์ธ์˜ ๋ฌด์„  ๋งˆ์šฐ์Šค',
        'category' => '์ปดํ“จํ„ฐ์ฃผ๋ณ€๊ธฐ๊ธฐ',
        'price' => 45000,
        'tags' => ['๋งˆ์šฐ์Šค', '๋ฌด์„ ', '์ธ์ฒด๊ณตํ•™'],
        'created_at' => date('Y-m-d H:i:s'),
        'rating' => 4.1,
        'stock' => 300
    ]
];

$params = ['body' => []];

foreach ($products as $product) {
    $params['body'][] = [
        'index' => [
            '_index' => 'products',
            '_id' => $product['id']
        ]
    ];
    $params['body'][] = $product;
}

try {
    $response = $client->bulk($params);
    
    if ($response['errors']) {
        echo "์ผ๋ถ€ ๋ฌธ์„œ ์ถ”๊ฐ€ ์‹คํŒจ\n";
    } else {
        echo "๋ชจ๋“  ๋ฌธ์„œ ์ถ”๊ฐ€ ์™„๋ฃŒ!\n";
    }
} catch (Exception $e) {
    echo "์—๋Ÿฌ: " . $e->getMessage();
}

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

โ€ข Bulk API ์‚ฌ์šฉ ์‹œ ํ•œ ๋ฒˆ์— 1000~5000๊ฐœ ์ •๋„๊ฐ€ ์ ๋‹นํ•ด
โ€ข ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ๊ณ , ๋„ˆ๋ฌด ์ ์œผ๋ฉด ํšจ์œจ์ด ๋–จ์–ด์ ธ
โ€ข ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜์ด ํ•„์š”ํ•˜๋ฉด refresh=true ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋˜, ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฃผ์˜ํ•ด์•ผ ํ•ด

MySQL ๋ฐ์ดํ„ฐ๋ฅผ Elasticsearch๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

๊ธฐ์กด MySQL ๋ฐ์ดํ„ฐ๋ฅผ Elasticsearch๋กœ ์˜ฎ๊ธฐ๋Š” ์‹ค์ „ ์˜ˆ์ œ์•ผ:

<?php
// MySQL ์—ฐ๊ฒฐ
$pdo = new PDO('mysql:host=localhost;dbname=myshop', 'username', 'password');

// ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
$stmt = $pdo->query("SELECT * FROM products");
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);

$params = ['body' => []];
$count = 0;

foreach ($products as $product) {
    $params['body'][] = [
        'index' => [
            '_index' => 'products',
            '_id' => $product['id']
        ]
    ];
    
    $params['body'][] = [
        'id' => (int)$product['id'],
        'name' => $product['name'],
        'description' => $product['description'],
        'category' => $product['category'],
        'price' => (int)$product['price'],
        'tags' => explode(',', $product['tags']),
        'created_at' => $product['created_at'],
        'rating' => (float)$product['rating'],
        'stock' => (int)$product['stock']
    ];
    
    $count++;
    
    // 1000๊ฐœ์”ฉ ๋ฌถ์–ด์„œ ์ „์†ก
    if ($count % 1000 == 0) {
        $client->bulk($params);
        $params = ['body' => []];
        echo "$count ๊ฐœ ์ฒ˜๋ฆฌ ์™„๋ฃŒ...\n";
    }
}

// ๋‚จ์€ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
if (!empty($params['body'])) {
    $client->bulk($params);
}

echo "์ด $count ๊ฐœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ!\n";

๐Ÿ”Ž ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ์ž‘์„ฑํ•˜๊ธฐ

๋“œ๋””์–ด ๊ฒ€์ƒ‰์˜ ํ•ต์‹ฌ ๋ถ€๋ถ„์ด์•ผ! Elasticsearch๋Š” ์ •๋ง ๋‹ค์–‘ํ•œ ๊ฒ€์ƒ‰ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ด. ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด์ž.

1๏ธโƒฃ ๊ธฐ๋ณธ ๋งค์น˜ ์ฟผ๋ฆฌ (Match Query)

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์ „๋ฌธ ๊ฒ€์ƒ‰์ด์•ผ. ์ž…๋ ฅํ•œ ํ…์ŠคํŠธ๋ฅผ ๋ถ„์„ํ•ด์„œ ๊ด€๋ จ ๋ฌธ์„œ๋ฅผ ์ฐพ์•„์ค˜.

<?php
// "๋ธ”๋ฃจํˆฌ์Šค ์ด์–ดํฐ" ๊ฒ€์ƒ‰
$params = [
    'index' => 'products',
    'body' => [
        'query' => [
            'match' => [
                'name' => '๋ธ”๋ฃจํˆฌ์Šค ์ด์–ดํฐ'
            ]
        ]
    ]
];

$response = $client->search($params);

foreach ($response['hits']['hits'] as $hit) {
    echo "์ƒํ’ˆ๋ช…: " . $hit['_source']['name'] . "\n";
    echo "์ ์ˆ˜: " . $hit['_score'] . "\n";
    echo "---\n";
}

"๋ธ”๋ฃจํˆฌ์Šค ์ด์–ดํฐ"์„ ๊ฒ€์ƒ‰ํ•˜๋ฉด "๋ธ”๋ฃจํˆฌ์Šค"๋‚˜ "์ด์–ดํฐ" ์ค‘ ํ•˜๋‚˜๋ผ๋„ ํฌํ•จ๋œ ๋ฌธ์„œ๋ฅผ ์ฐพ์•„์ค˜. ๋‘˜ ๋‹ค ํฌํ•จ๋œ ๋ฌธ์„œ๊ฐ€ ๋” ๋†’์€ ์ ์ˆ˜๋ฅผ ๋ฐ›์ง€!

2๏ธโƒฃ ๋ฉ€ํ‹ฐ ๋งค์น˜ ์ฟผ๋ฆฌ (Multi Match Query)

์—ฌ๋Ÿฌ ํ•„๋“œ๋ฅผ ๋™์‹œ์— ๊ฒ€์ƒ‰ํ•  ๋•Œ ์‚ฌ์šฉํ•ด. ์ œ๋ชฉ๊ณผ ์„ค๋ช…์„ ํ•จ๊ป˜ ๊ฒ€์ƒ‰ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์ž–์•„?

<?php
$params = [
    'index' => 'products',
    'body' => [
        'query' => [
            'multi_match' => [
                'query' => '๋ฌด์„  ๊ฒŒ์ด๋ฐ',
                'fields' => ['name^2', 'description'],  // name ํ•„๋“œ์— 2๋ฐฐ ๊ฐ€์ค‘์น˜
                'type' => 'best_fields'
            ]
        ]
    ]
];

$response = $client->search($params);

^2๋Š” ๊ฐ€์ค‘์น˜์•ผ. name ํ•„๋“œ์—์„œ ๋งค์นญ๋˜๋ฉด ์ ์ˆ˜๋ฅผ 2๋ฐฐ๋กœ ์˜ฌ๋ ค์ค˜. ์ œ๋ชฉ์ด ์„ค๋ช…๋ณด๋‹ค ์ค‘์š”ํ•˜๋‹ˆ๊นŒ!

3๏ธโƒฃ ๋ถˆ ์ฟผ๋ฆฌ (Bool Query)

์—ฌ๋Ÿฌ ์กฐ๊ฑด์„ ์กฐํ•ฉํ•  ๋•Œ ์‚ฌ์šฉํ•ด. must(AND), should(OR), must_not(NOT), filter ๊ฐ™์€ ์กฐ๊ฑด์„ ์„ž์„ ์ˆ˜ ์žˆ์–ด.

<?php
// "์ „์ž์ œํ’ˆ" ์นดํ…Œ๊ณ ๋ฆฌ์—์„œ "๋ฌด์„ "์ด ํฌํ•จ๋˜๊ณ , 
// ๊ฐ€๊ฒฉ์ด 100,000์› ์ดํ•˜์ธ ์ƒํ’ˆ ๊ฒ€์ƒ‰
$params = [
    'index' => 'products',
    'body' => [
        'query' => [
            'bool' => [
                'must' => [
                    ['match' => ['description' => '๋ฌด์„ ']]
                ],
                'filter' => [
                    ['term' => ['category' => '์ „์ž์ œํ’ˆ']],
                    ['range' => ['price' => ['lte' => 100000]]]
                ],
                'should' => [
                    ['match' => ['tags' => '๋ธ”๋ฃจํˆฌ์Šค']]
                ],
                'minimum_should_match' => 0
            ]
        ],
        'sort' => [
            ['rating' => ['order' => 'desc']],
            ['_score' => ['order' => 'desc']]
        ]
    ]
];

$response = $client->search($params);

๐Ÿ“Œ must vs filter ์ฐจ์ด์ 

โ€ข must: ์Šค์ฝ”์–ด ๊ณ„์‚ฐ์— ์˜ํ–ฅ์„ ์ค˜. ๊ด€๋ จ์„ฑ์ด ๋†’์„์ˆ˜๋ก ์ ์ˆ˜๊ฐ€ ์˜ฌ๋ผ๊ฐ€
โ€ข filter: ์Šค์ฝ”์–ด ๊ณ„์‚ฐ ์•ˆ ํ•ด. ๋‹จ์ˆœํžˆ ์กฐ๊ฑด ๋งŒ์กฑ ์—ฌ๋ถ€๋งŒ ์ฒดํฌํ•ด. ์บ์‹ฑ๋˜์–ด์„œ ๋” ๋นจ๋ผ!

์นดํ…Œ๊ณ ๋ฆฌ๋‚˜ ๊ฐ€๊ฒฉ ๊ฐ™์€ ์ •ํ™•ํ•œ ์กฐ๊ฑด์€ filter๋กœ, ํ…์ŠคํŠธ ๊ฒ€์ƒ‰์€ must๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ์ข‹์•„.

4๏ธโƒฃ ํผ์ง€ ์ฟผ๋ฆฌ (Fuzzy Query)

์˜คํƒ€๋ฅผ ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ๋˜‘๋˜‘ํ•œ ๊ฒ€์ƒ‰์ด์•ผ! ์‚ฌ์šฉ์ž๊ฐ€ "๋ธ”ํˆฌํˆฌ์Šค"๋ผ๊ณ  ์ž˜๋ชป ์ž…๋ ฅํ•ด๋„ "๋ธ”๋ฃจํˆฌ์Šค"๋ฅผ ์ฐพ์•„์ค˜.

<?php
$params = [
    'index' => 'products',
    'body' => [
        'query' => [
            'match' => [
                'name' => [
                    'query' => '๋ธ”ํˆฌํˆฌ์Šค',
                    'fuzziness' => 'AUTO'  // ์ž๋™์œผ๋กœ ํŽธ์ง‘ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ
                ]
            ]
        ]
    ]
];

$response = $client->search($params);

fuzziness๋Š” ์–ผ๋งˆ๋‚˜ ๋‹ค๋ฅธ ๊ธ€์ž๋ฅผ ํ—ˆ์šฉํ• ์ง€ ์ •ํ•ด. 'AUTO'๋กœ ์„ค์ •ํ•˜๋ฉด ๋‹จ์–ด ๊ธธ์ด์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ์กฐ์ ˆ๋ผ!

5๏ธโƒฃ ๊ตฌ๋ฌธ ๊ฒ€์ƒ‰ (Phrase Query)

์ •ํ™•ํ•œ ๊ตฌ๋ฌธ์„ ์ฐพ์„ ๋•Œ ์‚ฌ์šฉํ•ด. "๋ฌด์„  ์ด์–ดํฐ"์ด๋ผ๋Š” ์ˆœ์„œ ๊ทธ๋Œ€๋กœ ๋‚˜์˜ค๋Š” ๋ฌธ์„œ๋งŒ ์ฐพ์•„์ค˜.

<?php
$params = [
    'index' => 'products',
    'body' => [
        'query' => [
            'match_phrase' => [
                'description' => [
                    'query' => '๋…ธ์ด์ฆˆ ์บ”์Šฌ๋ง',
                    'slop' => 1  // ๋‹จ์–ด ์‚ฌ์ด์— 1๊ฐœ๊นŒ์ง€ ๋‹ค๋ฅธ ๋‹จ์–ด ํ—ˆ์šฉ
                ]
            ]
        ]
    ]
];

$response = $client->search($params);
๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ํƒ€์ž…๋ณ„ ํ™œ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค Match Query ์ผ๋ฐ˜์ ์ธ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ ์‚ฌ์šฉ ์˜ˆ์‹œ: "๋…ธํŠธ๋ถ ์ถ”์ฒœ" "์—ฌ๋ฆ„ ์›ํ”ผ์Šค" "PHP ๊ฐ•์ขŒ" Bool Query ๋ณตํ•ฉ ์กฐ๊ฑด ๊ฒ€์ƒ‰ ์‚ฌ์šฉ ์˜ˆ์‹œ: "์ „์ž์ œํ’ˆ + 10๋งŒ์› ์ดํ•˜" "์„œ์šธ + 2์ธ์‹ค + ์ฃผ์ฐจ๊ฐ€๋Šฅ" "PHP + ์ดˆ๊ธ‰ + ์˜จ๋ผ์ธ" Fuzzy Query ์˜คํƒ€ ํ—ˆ์šฉ ๊ฒ€์ƒ‰ ์‚ฌ์šฉ ์˜ˆ์‹œ: "์•„์ดํฐ" โ†’ "์•„์ดํด" "์‚ผ์„ฑ" โ†’ "์‚ผ์„ฑ" "Elasticsearch" โ†’ "Elasticsarch" Phrase Query ์ •ํ™•ํ•œ ๊ตฌ๋ฌธ ๊ฒ€์ƒ‰ ์‚ฌ์šฉ ์˜ˆ์‹œ: "๋…ธ์ด์ฆˆ ์บ”์Šฌ๋ง" "๋ฌด๋ฃŒ ๋ฐฐ์†ก" "๋‹น์ผ ์ถœ๊ณ " Range Query ๋ฒ”์œ„ ๊ฒ€์ƒ‰ ์‚ฌ์šฉ ์˜ˆ์‹œ: "๊ฐ€๊ฒฉ 5๋งŒ์›~10๋งŒ์›" "ํ‰์  4.0 ์ด์ƒ" "์ตœ๊ทผ 1์ฃผ์ผ ์ด๋‚ด" Wildcard Query ํŒจํ„ด ๋งค์นญ ๊ฒ€์ƒ‰ ์‚ฌ์šฉ ์˜ˆ์‹œ: "๊ฐค๋Ÿญ์‹œ*" โ†’ ๊ฐค๋Ÿญ์‹œS, ๊ฐค๋Ÿญ์‹œ๋…ธํŠธ "*ํฐ" โ†’ ์•„์ดํฐ, ๊ฐค๋Ÿญ์‹œํฐ "PHP*" โ†’ PHP7, PHP8 ๐Ÿ’ก ์ƒํ™ฉ์— ๋งž๋Š” ์ฟผ๋ฆฌ ํƒ€์ž…์„ ์„ ํƒํ•˜๋ฉด ๊ฒ€์ƒ‰ ํ’ˆ์งˆ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค!

๐ŸŽจ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ•˜์ด๋ผ์ดํŒ…

๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—์„œ ๊ฒ€์ƒ‰์–ด๋ฅผ ๊ฐ•์กฐ ํ‘œ์‹œํ•˜๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ›จ์”ฌ ์ข‹์•„์ ธ. ๋„ค์ด๋ฒ„๋‚˜ ๊ตฌ๊ธ€์—์„œ ๊ฒ€์ƒ‰์–ด๊ฐ€ ๊ตต๊ฒŒ ํ‘œ์‹œ๋˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ง์ด์•ผ!

<?php
$params = [
    'index' => 'products',
    'body' => [
        'query' => [
            'multi_match' => [
                'query' => '๋ฌด์„  ๋ธ”๋ฃจํˆฌ์Šค',
                'fields' => ['name', 'description']
            ]
        ],
        'highlight' => [
            'fields' => [
                'name' => [
                    'pre_tags' => ['<strong class="highlight">'],
                    'post_tags' => ['</strong>']
                ],
                'description' => [
                    'pre_tags' => ['<strong class="highlight">'],
                    'post_tags' => ['</strong>'],
                    'fragment_size' => 150,  // ํ‘œ์‹œํ•  ๋ฌธ์ž ์ˆ˜
                    'number_of_fragments' => 3  // ์ตœ๋Œ€ ์กฐ๊ฐ ์ˆ˜
                ]
            ]
        ]
    ]
];

$response = $client->search($params);

foreach ($response['hits']['hits'] as $hit) {
    echo "์ƒํ’ˆ๋ช…: ";
    if (isset($hit['highlight']['name'])) {
        echo $hit['highlight']['name'][0];  // ํ•˜์ด๋ผ์ดํŠธ๋œ ์ œ๋ชฉ
    } else {
        echo $hit['_source']['name'];
    }
    echo "\n";
    
    if (isset($hit['highlight']['description'])) {
        echo "์„ค๋ช…: " . implode(' ... ', $hit['highlight']['description']) . "\n";
    }
    echo "---\n";
}

โœจ ํ•˜์ด๋ผ์ดํŒ… ๊ฒฐ๊ณผ ์˜ˆ์‹œ

์ƒํ’ˆ๋ช…: ๋ฌด์„  ๋ธ”๋ฃจํˆฌ์Šค ์ด์–ดํฐ
์„ค๋ช…: ๊ณ ์Œ์งˆ ๋ฌด์„  ์ด์–ดํฐ์œผ๋กœ ์ตœ๋Œ€ 24์‹œ๊ฐ„ ์žฌ์ƒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค ... ๋ธ”๋ฃจํˆฌ์Šค 5.0์„ ์ง€์›ํ•˜์—ฌ ์•ˆ์ •์ ์ธ ์—ฐ๊ฒฐ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค

๐Ÿ“Š ์ง‘๊ณ„(Aggregation)๋กœ ํ†ต๊ณ„ ๋‚ด๊ธฐ

Elasticsearch์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ ์ง‘๊ณ„์•ผ! ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ํ†ต๊ณ„๋ฅผ ๋‚ผ ์ˆ˜ ์žˆ์–ด. ์‡ผํ•‘๋ชฐ์˜ ํ•„ํ„ฐ ๊ธฐ๋Šฅ ๊ฐ™์€ ๊ฑฐ ์ƒ๊ฐํ•˜๋ฉด ๋ผ.

์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ƒํ’ˆ ์ˆ˜ ์ง‘๊ณ„

<?php
$params = [
    'index' => 'products',
    'body' => [
        'size' => 0,  // ๋ฌธ์„œ๋Š” ๊ฐ€์ ธ์˜ค์ง€ ์•Š๊ณ  ์ง‘๊ณ„๋งŒ
        'aggs' => [
            'categories' => [
                'terms' => [
                    'field' => 'category',
                    'size' => 10
                ]
            ]
        ]
    ]
];

$response = $client->search($params);

echo "์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ƒํ’ˆ ์ˆ˜:\n";
foreach ($response['aggregations']['categories']['buckets'] as $bucket) {
    echo $bucket['key'] . ": " . $bucket['doc_count'] . "๊ฐœ\n";
}

๊ฐ€๊ฒฉ๋Œ€๋ณ„ ๋ถ„ํฌ ์ง‘๊ณ„

<?php
$params = [
    'index' => 'products',
    'body' => [
        'size' => 0,
        'aggs' => [
            'price_ranges' => [
                'range' => [
                    'field' => 'price',
                    'ranges' => [
                        ['to' => 50000],
                        ['from' => 50000, 'to' => 100000],
                        ['from' => 100000, 'to' => 200000],
                        ['from' => 200000]
                    ]
                ]
            ],
            'avg_price' => [
                'avg' => [
                    'field' => 'price'
                ]
            ],
            'max_price' => [
                'max' => [
                    'field' => 'price'
                ]
            ],
            'min_price' => [
                'min' => [
                    'field' => 'price'
                ]
            ]
        ]
    ]
];

$response = $client->search($params);

echo "๊ฐ€๊ฒฉ๋Œ€๋ณ„ ์ƒํ’ˆ ์ˆ˜:\n";
foreach ($response['aggregations']['price_ranges']['buckets'] as $bucket) {
    $from = $bucket['from'] ?? 0;
    $to = $bucket['to'] ?? '์ด์ƒ';
    echo number_format($from) . "์› ~ " . (is_numeric($to) ? number_format($to) . "์›" : $to) . ": ";
    echo $bucket['doc_count'] . "๊ฐœ\n";
}

echo "\nํ†ต๊ณ„:\n";
echo "ํ‰๊ท  ๊ฐ€๊ฒฉ: " . number_format($response['aggregations']['avg_price']['value']) . "์›\n";
echo "์ตœ๊ณ  ๊ฐ€๊ฒฉ: " . number_format($response['aggregations']['max_price']['value']) . "์›\n";
echo "์ตœ์ € ๊ฐ€๊ฒฉ: " . number_format($response['aggregations']['min_price']['value']) . "์›\n";

์ค‘์ฒฉ ์ง‘๊ณ„: ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ‰๊ท  ๊ฐ€๊ฒฉ

<?php
$params = [
    'index' => 'products',
    'body' => [
        'size' => 0,
        'aggs' => [
            'categories' => [
                'terms' => [
                    'field' => 'category'
                ],
                'aggs' => [
                    'avg_price' => [
                        'avg' => [
                            'field' => 'price'
                        ]
                    ],
                    'avg_rating' => [
                        'avg' => [
                            'field' => 'rating'
                        ]
                    ]
                ]
            ]
        ]
    ]
];

$response = $client->search($params);

echo "์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ†ต๊ณ„:\n";
foreach ($response['aggregations']['categories']['buckets'] as $bucket) {
    echo "\n" . $bucket['key'] . ":\n";
    echo "  ์ƒํ’ˆ ์ˆ˜: " . $bucket['doc_count'] . "๊ฐœ\n";
    echo "  ํ‰๊ท  ๊ฐ€๊ฒฉ: " . number_format($bucket['avg_price']['value']) . "์›\n";
    echo "  ํ‰๊ท  ํ‰์ : " . round($bucket['avg_rating']['value'], 2) . "์ \n";
}

๐ŸŽฏ ์ง‘๊ณ„ ํ™œ์šฉ ์•„์ด๋””์–ด

โ€ข ์‡ผํ•‘๋ชฐ ํ•„ํ„ฐ: ์นดํ…Œ๊ณ ๋ฆฌ, ๋ธŒ๋žœ๋“œ, ๊ฐ€๊ฒฉ๋Œ€๋ณ„ ์ƒํ’ˆ ์ˆ˜ ํ‘œ์‹œ
โ€ข ๋Œ€์‹œ๋ณด๋“œ: ์ผ๋ณ„/์›”๋ณ„ ํ†ต๊ณ„, ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ์ˆœ์œ„
โ€ข ์ถ”์ฒœ ์‹œ์Šคํ…œ: ๋น„์Šทํ•œ ๊ฐ€๊ฒฉ๋Œ€/์นดํ…Œ๊ณ ๋ฆฌ ์ƒํ’ˆ ์ถ”์ฒœ
โ€ข ํŠธ๋ Œ๋“œ ๋ถ„์„: ์‹œ๊ฐ„๋Œ€๋ณ„ ๊ฒ€์ƒ‰ ํŒจํ„ด ๋ถ„์„

๐Ÿ”ง ์‹ค์ „ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ

์ด์ œ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ๋ฐ”๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์ด์•ผ.

๊ฒ€์ƒ‰ ํด๋ž˜์Šค ๋งŒ๋“ค๊ธฐ

<?php
class ProductSearch {
    private $client;
    private $index = 'products';
    
    public function __construct($hosts = ['localhost:9200']) {
        $this->client = ClientBuilder::create()
            ->setHosts($hosts)
            ->build();
    }
    
    /**
     * ํ†ตํ•ฉ ๊ฒ€์ƒ‰ ๋ฉ”์„œ๋“œ
     */
    public function search($keyword, $filters = [], $options = []) {
        $query = $this->buildQuery($keyword, $filters);
        
        $params = [
            'index' => $this->index,
            'body' => [
                'query' => $query,
                'from' => $options['from'] ?? 0,
                'size' => $options['size'] ?? 20,
                'sort' => $this->buildSort($options['sort'] ?? 'relevance'),
                'highlight' => [
                    'fields' => [
                        'name' => new \stdClass(),
                        'description' => [
                            'fragment_size' => 150,
                            'number_of_fragments' => 2
                        ]
                    ]
                ]
            ]
        ];
        
        // ์ง‘๊ณ„ ์ถ”๊ฐ€
        if (!empty($options['aggregations'])) {
            $params['body']['aggs'] = $this->buildAggregations();
        }
        
        return $this->client->search($params);
    }
    
    /**
     * ์ฟผ๋ฆฌ ๋นŒ๋”
     */
    private function buildQuery($keyword, $filters) {
        $must = [];
        $filter = [];
        
        // ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰
        if (!empty($keyword)) {
            $must[] = [
                'multi_match' => [
                    'query' => $keyword,
                    'fields' => ['name^3', 'description', 'tags^2'],
                    'type' => 'best_fields',
                    'fuzziness' => 'AUTO'
                ]
            ];
        }
        
        // ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ
        if (!empty($filters['category'])) {
            $filter[] = ['term' => ['category' => $filters['category']]];
        }
        
        // ๊ฐ€๊ฒฉ ๋ฒ”์œ„ ํ•„ํ„ฐ
        if (!empty($filters['price_min']) || !empty($filters['price_max'])) {
            $range = [];
            if (!empty($filters['price_min'])) {
                $range['gte'] = $filters['price_min'];
            }
            if (!empty($filters['price_max'])) {
                $range['lte'] = $filters['price_max'];
            }
            $filter[] = ['range' => ['price' => $range]];
        }
        
        // ํ‰์  ํ•„ํ„ฐ
        if (!empty($filters['rating_min'])) {
            $filter[] = ['range' => ['rating' => ['gte' => $filters['rating_min']]]];
        }
        
        // ์žฌ๊ณ  ์žˆ๋Š” ์ƒํ’ˆ๋งŒ
        if (!empty($filters['in_stock'])) {
            $filter[] = ['range' => ['stock' => ['gt' => 0]]];
        }
        
        // Bool ์ฟผ๋ฆฌ ๊ตฌ์„ฑ
        $bool = [];
        if (!empty($must)) {
            $bool['must'] = $must;
        }
        if (!empty($filter)) {
            $bool['filter'] = $filter;
        }
        
        // ํ‚ค์›Œ๋“œ๊ฐ€ ์—†์œผ๋ฉด ์ „์ฒด ๊ฒ€์ƒ‰
        if (empty($bool)) {
            return ['match_all' => new \stdClass()];
        }
        
        return ['bool' => $bool];
    }
    
    /**
     * ์ •๋ ฌ ์˜ต์…˜ ๋นŒ๋”
     */
    private function buildSort($sortType) {
        switch ($sortType) {
            case 'price_asc':
                return [['price' => ['order' => 'asc']]];
            case 'price_desc':
                return [['price' => ['order' => 'desc']]];
            case 'rating':
                return [['rating' => ['order' => 'desc']], ['_score' => ['order' => 'desc']]];
            case 'newest':
                return [['created_at' => ['order' => 'desc']]];
            case 'relevance':
            default:
                return [['_score' => ['order' => 'desc']]];
        }
    }
    
    /**
     * ์ง‘๊ณ„ ๋นŒ๋”
     */
    private function buildAggregations() {
        return [
            'categories' => [
                'terms' => ['field' => 'category', 'size' => 20]
            ],
            'price_ranges' => [
                'range' => [
                    'field' => 'price',
                    'ranges' => [
                        ['key' => '5๋งŒ์› ๋ฏธ๋งŒ', 'to' => 50000],
                        ['key' => '5๋งŒ์›~10๋งŒ์›', 'from' => 50000, 'to' => 100000],
                        ['key' => '10๋งŒ์›~20๋งŒ์›', 'from' => 100000, 'to' => 200000],
                        ['key' => '20๋งŒ์› ์ด์ƒ', 'from' => 200000]
                    ]
                ]
            ],
            'avg_price' => [
                'avg' => ['field' => 'price']
            ]
        ];
    }
    
    /**
     * ์ž๋™์™„์„ฑ ์ œ์•ˆ
     */
    public function suggest($prefix) {
        $params = [
            'index' => $this->index,
            'body' => [
                'suggest' => [
                    'product-suggest' => [
                        'prefix' => $prefix,
                        'completion' => [
                            'field' => 'name.suggest',
                            'size' => 10,
                            'fuzzy' => [
                                'fuzziness' => 'AUTO'
                            ]
                        ]
                    ]
                ]
            ]
        ];
        
        return $this->client->search($params);
    }
}

๊ฒ€์ƒ‰ API ์—”๋“œํฌ์ธํŠธ ๋งŒ๋“ค๊ธฐ

<?php
// search_api.php
require 'vendor/autoload.php';
require 'ProductSearch.php';

header('Content-Type: application/json; charset=utf-8');

$search = new ProductSearch();

// GET ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ›๊ธฐ
$keyword = $_GET['q'] ?? '';
$page = max(1, intval($_GET['page'] ?? 1));
$perPage = min(100, max(1, intval($_GET['per_page'] ?? 20)));
$sort = $_GET['sort'] ?? 'relevance';

// ํ•„ํ„ฐ ์„ค์ •
$filters = [];
if (!empty($_GET['category'])) {
    $filters['category'] = $_GET['category'];
}
if (!empty($_GET['price_min'])) {
    $filters['price_min'] = intval($_GET['price_min']);
}
if (!empty($_GET['price_max'])) {
    $filters['price_max'] = intval($_GET['price_max']);
}
if (!empty($_GET['rating_min'])) {
    $filters['rating_min'] = floatval($_GET['rating_min']);
}
if (!empty($_GET['in_stock'])) {
    $filters['in_stock'] = true;
}

// ๊ฒ€์ƒ‰ ์˜ต์…˜
$options = [
    'from' => ($page - 1) * $perPage,
    'size' => $perPage,
    'sort' => $sort,
    'aggregations' => true
];

try {
    $response = $search->search($keyword, $filters, $options);
    
    // ๊ฒฐ๊ณผ ํฌ๋งทํŒ…
    $result = [
        'success' => true,
        'total' => $response['hits']['total']['value'],
        'took' => $response['took'],
        'page' => $page,
        'per_page' => $perPage,
        'items' => [],
        'aggregations' => []
    ];
    
    // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
    foreach ($response['hits']['hits'] as $hit) {
        $item = $hit['_source'];
        $item['score'] = $hit['_score'];
        
        // ํ•˜์ด๋ผ์ดํŠธ ์ ์šฉ
        if (isset($hit['highlight'])) {
            if (isset($hit['highlight']['name'])) {
                $item['name_highlighted'] = $hit['highlight']['name'][0];
            }
            if (isset($hit['highlight']['description'])) {
                $item['description_highlighted'] = implode(' ... ', $hit['highlight']['description']);
            }
        }
        
        $result['items'][] = $item;
    }
    
    // ์ง‘๊ณ„ ๊ฒฐ๊ณผ
    if (isset($response['aggregations'])) {
        $result['aggregations'] = [
            'categories' => $response['aggregations']['categories']['buckets'],
            'price_ranges' => $response['aggregations']['price_ranges']['buckets'],
            'avg_price' => $response['aggregations']['avg_price']['value']
        ];
    }
    
    echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    
} catch (Exception $e) {
    http_response_code(500);
    echo json_encode([
        'success' => false,
        'error' => $e->getMessage()
    ], JSON_UNESCAPED_UNICODE);
}

ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™ ์˜ˆ์ œ

<!DOCTYPE html>
<html>
<head>
    <title>์ƒํ’ˆ ๊ฒ€์ƒ‰</title>
    <style>
        .search-box { margin: 20px; }
        .filters { margin: 20px; }
        .results { margin: 20px; }
        .product-item { 
            border: 1px solid #ddd; 
            padding: 15px; 
            margin: 10px 0;
            border-radius: 8px;
        }
        .highlight { 
            background: #fff9c4; 
            font-weight: bold; 
        }
    </style>
</head>
<body>
    <div class="search-box">
        <input type="text" id="searchInput" placeholder="์ƒํ’ˆ ๊ฒ€์ƒ‰..." style="width: 400px; padding: 10px;">
        <button onclick="search()">๊ฒ€์ƒ‰</button>
    </div>
    
    <div class="filters">
        <select id="categoryFilter">
            <option value="">์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ</option>
        </select>
        
        <select id="sortFilter">
            <option value="relevance">๊ด€๋ จ๋„์ˆœ</option>
            <option value="price_asc">๊ฐ€๊ฒฉ ๋‚ฎ์€์ˆœ</option>
            <option value="price_desc">๊ฐ€๊ฒฉ ๋†’์€์ˆœ</option>
            <option value="rating">ํ‰์ ์ˆœ</option>
            <option value="newest">์ตœ์‹ ์ˆœ</option>
        </select>
        
        <label>
            <input type="checkbox" id="inStockFilter"> ์žฌ๊ณ  ์žˆ๋Š” ์ƒํ’ˆ๋งŒ
        </label>
    </div>
    
    <div class="results" id="results"></div>
    
    <script>
    async function search() {
        const keyword = document.getElementById('searchInput').value;
        const category = document.getElementById('categoryFilter').value;
        const sort = document.getElementById('sortFilter').value;
        const inStock = document.getElementById('inStockFilter').checked;
        
        const params = new URLSearchParams({
            q: keyword,
            sort: sort,
            per_page: 20
        });
        
        if (category) params.append('category', category);
        if (inStock) params.append('in_stock', '1');
        
        try {
            const response = await fetch(`search_api.php?${params}`);
            const data = await response.json();
            
            displayResults(data);
            updateFilters(data.aggregations);
        } catch (error) {
            console.error('๊ฒ€์ƒ‰ ์˜ค๋ฅ˜:', error);
        }
    }
    
    function displayResults(data) {
        const resultsDiv = document.getElementById('results');
        
        if (data.total === 0) {
            resultsDiv.innerHTML = '<p>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>';
            return;
        }
        
        let html = `<p>์ด ${data.total}๊ฐœ์˜ ์ƒํ’ˆ (${data.took}ms)</p>`;
        
        data.items.forEach(item => {
            const name = item.name_highlighted || item.name;
            const desc = item.description_highlighted || item.description.substring(0, 150);
            
            html += `
                <div class="product-item">
                    <h3>${name}</h3>
                    <p>${desc}</p>
                    <p>
                        <strong>๊ฐ€๊ฒฉ:</strong> ${item.price.toLocaleString()}์› | 
                        <strong>ํ‰์ :</strong> ${item.rating}์  | 
                        <strong>์žฌ๊ณ :</strong> ${item.stock}๊ฐœ
                    </p>
                    <p><small>์นดํ…Œ๊ณ ๋ฆฌ: ${item.category} | ํƒœ๊ทธ: ${item.tags.join(', ')}</small></p>
                </div>
            `;
        });
        
        resultsDiv.innerHTML = html;
    }
    
    function updateFilters(aggregations) {
        if (!aggregations) return;
        
        // ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ ์—…๋ฐ์ดํŠธ
        const categoryFilter = document.getElementById('categoryFilter');
        const currentCategory = categoryFilter.value;
        
        categoryFilter.innerHTML = '<option value="">์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ</option>';
        
        aggregations.categories.forEach(bucket => {
            const option = document.createElement('option');
            option.value = bucket.key;
            option.textContent = `${bucket.key} (${bucket.doc_count})`;
            if (bucket.key === currentCategory) {
                option.selected = true;
            }
            categoryFilter.appendChild(option);
        });
    }
    
    // ์—”ํ„ฐํ‚ค๋กœ ๊ฒ€์ƒ‰
    document.getElementById('searchInput').addEventListener('keypress', function(e) {
        if (e.key === 'Enter') {
            search();
        }
    });
    
    // ํ•„ํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ๊ฒ€์ƒ‰
    document.getElementById('categoryFilter').addEventListener('change', search);
    document.getElementById('sortFilter').addEventListener('change', search);
    document.getElementById('inStockFilter').addEventListener('change', search);
    </script>
</body>
</html>

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ์ด ๋А๋ ค์ง€๋ฉด ์‚ฌ์šฉ์ž๋“ค์ด ๋– ๋‚˜๊ฐ€. ๋น ๋ฅด๊ณ  ํšจ์œจ์ ์ธ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ์ตœ์ ํ™” ๋ฐฉ๋ฒ•๋“ค์„ ์•Œ์•„๋ณด์ž!

๐ŸŽฏ ์ธ๋ฑ์‹ฑ ์ตœ์ ํ™”

Bulk API ํ™œ์šฉ
ํ•œ ๋ฒˆ์— ์—ฌ๋Ÿฌ ๋ฌธ์„œ๋ฅผ ์ƒ‰์ธํ•˜๋ฉด ๋„คํŠธ์›Œํฌ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์ค„์–ด๋“ค์–ด. 1000~5000๊ฐœ์”ฉ ๋ฌถ์–ด์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ์ข‹์•„.

Refresh ๊ฐ„๊ฒฉ ์กฐ์ •
์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰์ด ํ•„์š” ์—†๋‹ค๋ฉด refresh_interval์„ ๋Š˜๋ ค์„œ ์„ฑ๋Šฅ์„ ๋†’์ผ ์ˆ˜ ์žˆ์–ด.

PUT /products/_settings
{
  "index": {
    "refresh_interval": "30s"
  }
}

๐Ÿ’พ ์ฟผ๋ฆฌ ์บ์‹ฑ

Filter Context ์‚ฌ์šฉ
filter๋Š” ์บ์‹ฑ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ˜๋ณต์ ์ธ ์ฟผ๋ฆฌ๊ฐ€ ๋นจ๋ผ์ ธ. ์นดํ…Œ๊ณ ๋ฆฌ, ๊ฐ€๊ฒฉ ๋ฒ”์œ„ ๊ฐ™์€ ๊ฑด filter๋กœ ์ฒ˜๋ฆฌํ•ด.

Request Cache
size=0์ธ ์ง‘๊ณ„ ์ฟผ๋ฆฌ๋Š” ์ž๋™์œผ๋กœ ์บ์‹ฑ๋ผ. ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ๋น ๋ฅด๊ฒŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์ง€.

๐Ÿ” ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ์ตœ์ ํ™”

ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ
_source ํ•„ํ„ฐ๋ง์œผ๋กœ ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ๊ฐ€์ ธ์˜ค๋ฉด ๋„คํŠธ์›Œํฌ ์ „์†ก๋Ÿ‰์ด ์ค„์–ด๋“ค์–ด.

"_source": ["name", "price", "rating"]

from + size ์ œํ•œ
๊นŠ์€ ํŽ˜์ด์ง€๋„ค์ด์…˜์€ ๋А๋ ค. search_after๋‚˜ scroll API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„.

๐Ÿ“Š ์ƒค๋“œ ์„ค์ •

์ ์ ˆํ•œ ์ƒค๋“œ ์ˆ˜
๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ํฌ๊ณ , ๋„ˆ๋ฌด ์ ์œผ๋ฉด ํ™•์žฅ์„ฑ์ด ๋–จ์–ด์ ธ. ์ƒค๋“œ๋‹น 20~50GB๊ฐ€ ์ ๋‹นํ•ด.

๋ ˆํ”Œ๋ฆฌ์นด ํ™œ์šฉ
์ฝ๊ธฐ ์„ฑ๋Šฅ์„ ๋†’์ด๋ ค๋ฉด ๋ ˆํ”Œ๋ฆฌ์นด๋ฅผ ๋Š˜๋ ค. ๊ฒ€์ƒ‰ ์š”์ฒญ์ด ์—ฌ๋Ÿฌ ๋…ธ๋“œ์— ๋ถ„์‚ฐ๋ผ.

โš ๏ธ ํ”ผํ•ด์•ผ ํ•  ์•ˆํ‹ฐํŒจํ„ด

1. Wildcard ์ฟผ๋ฆฌ ๋‚จ์šฉ
*๊ฒ€์ƒ‰์–ด* ๊ฐ™์€ ํŒจํ„ด์€ ๋งค์šฐ ๋А๋ ค. ๊ฐ€๋Šฅํ•˜๋ฉด n-gram์ด๋‚˜ edge n-gram์„ ์‚ฌ์šฉํ•ด.

2. ๋„ˆ๋ฌด ๋งŽ์€ ํ•„๋“œ ๊ฒ€์ƒ‰
multi_match์—์„œ ์ˆ˜์‹ญ ๊ฐœ ํ•„๋“œ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋ฉด ๋А๋ ค์ ธ. ์ค‘์š”ํ•œ ํ•„๋“œ 3~5๊ฐœ๋งŒ ์„ ํƒํ•ด.

3. ๊นŠ์€ ํŽ˜์ด์ง€๋„ค์ด์…˜
from=10000 ๊ฐ™์€ ๊ฑด ํ”ผํ•ด. 10,000๊ฐœ ๋ฌธ์„œ๋ฅผ ๋ชจ๋‘ ์ •๋ ฌํ•ด์•ผ ํ•˜๊ฑฐ๋“ .

4. ์‹ค์‹œ๊ฐ„ refresh
๋งค๋ฒˆ refresh=true๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ๋–จ์–ด์ ธ. ๊ผญ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋งŒ ์‚ฌ์šฉํ•ด.

โœ… ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง

<?php
// ๋А๋ฆฐ ์ฟผ๋ฆฌ ๋กœ๊น…
$params = [
    'index' => 'products',
    'body' => [
        'profile' => true,  // ์ฟผ๋ฆฌ ํ”„๋กœํŒŒ์ผ๋ง ํ™œ์„ฑํ™”
        'query' => [
            'match' => ['name' => '๊ฒ€์ƒ‰์–ด']
        ]
    ]
];

$response = $client->search($params);

// ํ”„๋กœํŒŒ์ผ ์ •๋ณด ํ™•์ธ
if (isset($response['profile'])) {
    $profile = $response['profile'];
    // ๊ฐ ์ƒค๋“œ๋ณ„ ์‹คํ–‰ ์‹œ๊ฐ„ ๋ถ„์„
    foreach ($profile['shards'] as $shard) {
        foreach ($shard['searches'] as $search) {
            echo "์ฟผ๋ฆฌ ์‹œ๊ฐ„: " . $search['query'][0]['time_in_nanos'] / 1000000 . "ms\n";
        }
    }
}

๐Ÿ›ก๏ธ ๋ณด์•ˆ๊ณผ ์šด์˜ ๊ด€๋ฆฌ

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ณด์•ˆ์ด ์ •๋ง ์ค‘์š”ํ•ด! ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ์šด์˜ํ•˜๋ฉด ํฐ์ผ ๋‚˜.

๐Ÿ” ํ•„์ˆ˜ ๋ณด์•ˆ ์„ค์ •

1. X-Pack Security ํ™œ์„ฑํ™”

# elasticsearch.yml
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.http.ssl.enabled: true

2. ์‚ฌ์šฉ์ž ์ธ์ฆ ์„ค์ •

<?php
$client = ClientBuilder::create()
    ->setHosts(['https://localhost:9200'])
    ->setBasicAuthentication('username', 'password')
    ->setSSLVerification(false)  // ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ
    ->build();

3. ์—ญํ•  ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด
์ฝ๊ธฐ ์ „์šฉ ์‚ฌ์šฉ์ž, ์“ฐ๊ธฐ ๊ถŒํ•œ ์‚ฌ์šฉ์ž๋ฅผ ๋ถ„๋ฆฌํ•ด์„œ ๊ด€๋ฆฌํ•ด. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋Š” ์ตœ์†Œ ๊ถŒํ•œ๋งŒ ๋ถ€์—ฌํ•˜๋Š” ๊ฒŒ ์›์น™์ด์•ผ.

๐Ÿ“ˆ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…

ํด๋Ÿฌ์Šคํ„ฐ ์ƒํƒœ ํ™•์ธ

<?php
// ํด๋Ÿฌ์Šคํ„ฐ ํ—ฌ์Šค ์ฒดํฌ
$health = $client->cluster()->health();

if ($health['status'] === 'red') {
    // ์•Œ๋ฆผ ๋ฐœ์†ก
    error_log('Elasticsearch cluster is RED!');
}

// ๋…ธ๋“œ ํ†ต๊ณ„
$stats = $client->nodes()->stats();
foreach ($stats['nodes'] as $node) {
    $jvmHeapPercent = $node['jvm']['mem']['heap_used_percent'];
    if ($jvmHeapPercent > 85) {
        error_log("Node {$node['name']} heap usage: {$jvmHeapPercent}%");
    }
}

์ธ๋ฑ์Šค ๊ด€๋ฆฌ

<?php
// ์˜ค๋ž˜๋œ ์ธ๋ฑ์Šค ์‚ญ์ œ (๋กœ๊ทธ ๋ฐ์ดํ„ฐ ๋“ฑ)
$indices = $client->cat()->indices(['format' => 'json']);

foreach ($indices as $index) {
    $indexName = $index['index'];
    
    // logs-2024-01-01 ํ˜•์‹์˜ ์ธ๋ฑ์Šค์—์„œ ๋‚ ์งœ ์ถ”์ถœ
    if (preg_match('/logs-(\d{4}-\d{2}-\d{2})/', $indexName, $matches)) {
        $date = new DateTime($matches[1]);
        $now = new DateTime();
        $diff = $now->diff($date)->days;
        
        // 30์ผ ์ด์ƒ ๋œ ์ธ๋ฑ์Šค ์‚ญ์ œ
        if ($diff > 30) {
            $client->indices()->delete(['index' => $indexName]);
            echo "Deleted old index: $indexName\n";
        }
    }
}

๐Ÿ’ก ๋ฐฑ์—…๊ณผ ๋ณต๊ตฌ

์Šค๋ƒ…์ƒท ์ƒ์„ฑ

<?php
// ์Šค๋ƒ…์ƒท ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๋“ฑ๋ก
$client->snapshot()->createRepository([
    'repository' => 'my_backup',
    'body' => [
        'type' => 'fs',
        'settings' => [
            'location' => '/mount/backups/elasticsearch'
        ]
    ]
]);

// ์Šค๋ƒ…์ƒท ์ƒ์„ฑ
$client->snapshot()->create([
    'repository' => 'my_backup',
    'snapshot' => 'snapshot_' . date('Y-m-d'),
    'body' => [
        'indices' => 'products',
        'ignore_unavailable' => true,
        'include_global_state' => false
    ]
]);

์ •๊ธฐ์ ์œผ๋กœ ์Šค๋ƒ…์ƒท์„ ์ƒ์„ฑํ•ด์„œ ๋ฐ์ดํ„ฐ ์†์‹ค์— ๋Œ€๋น„ํ•ด์•ผ ํ•ด. cron์œผ๋กœ ์ž๋™ํ™”ํ•˜๋Š” ๊ฒŒ ์ข‹์•„!

๐Ÿš€ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ํ™œ์šฉํ•˜๊ธฐ

๊ธฐ๋ณธ ๊ฒ€์ƒ‰์„ ๋„˜์–ด์„œ ๋” ๋˜‘๋˜‘ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž!

๐Ÿค– ์ž๋™์™„์„ฑ (Autocomplete)

์‚ฌ์šฉ์ž๊ฐ€ ํƒ€์ดํ•‘ํ•˜๋Š” ๋™์•ˆ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ œ์•ˆ์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด์•ผ. ๊ฒ€์ƒ‰ ๊ฒฝํ—˜์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œ์ผœ์ค˜!

<?php
// ์ธ๋ฑ์Šค ์ƒ์„ฑ ์‹œ suggest ํ•„๋“œ ์ถ”๊ฐ€
$params = [
    'index' => 'products',
    'body' => [
        'mappings' => [
            'properties' => [
                'name' => [
                    'type' => 'text',
                    'fields' => [
                        'suggest' => [
                            'type' => 'completion'
                        ]
                    ]
                ]
            ]
        ]
    ]
];

// ์ž๋™์™„์„ฑ ๊ฒ€์ƒ‰
function autocomplete($client, $prefix) {
    $params = [
        'index' => 'products',
        'body' => [
            'suggest' => [
                'product-suggest' => [
                    'prefix' => $prefix,
                    'completion' => [
                        'field' => 'name.suggest',
                        'size' => 10,
                        'skip_duplicates' => true,
                        'fuzzy' => [
                            'fuzziness' => 1
                        ]
                    ]
                ]
            ]
        ]
    ];
    
    $response = $client->search($params);
    $suggestions = [];
    
    foreach ($response['suggest']['product-suggest'][0]['options'] as $option) {
        $suggestions[] = $option['text'];
    }
    
    return $suggestions;
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$suggestions = autocomplete($client, '๋ธ”๋ฃจ');
// ๊ฒฐ๊ณผ: ['๋ธ”๋ฃจํˆฌ์Šค ์ด์–ดํฐ', '๋ธ”๋ฃจํˆฌ์Šค ์Šคํ”ผ์ปค', '๋ธ”๋ฃจํˆฌ์Šค ํ‚ค๋ณด๋“œ']

๐ŸŽฏ ๊ฐœ์ธํ™” ๊ฒ€์ƒ‰

์‚ฌ์šฉ์ž์˜ ๊ณผ๊ฑฐ ํ–‰๋™์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์–ด. ๋” ๊ด€๋ จ์„ฑ ๋†’์€ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฑฐ์ง€!

<?php
function personalizedSearch($client, $keyword, $userId) {
    // ์‚ฌ์šฉ์ž์˜ ๊ณผ๊ฑฐ ๊ตฌ๋งค/์กฐํšŒ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ
    $userPreferences = getUserPreferences($userId);
    
    $params = [
        'index' => 'products',
        'body' => [
            'query' => [
                'bool' => [
                    'must' => [
                        ['multi_match' => [
                            'query' => $keyword,
                            'fields' => ['name^3', 'description']
                        ]]
                    ],
                    'should' => [
                        // ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํ˜ธํ•˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ์— ๊ฐ€์ค‘์น˜
                        ['terms' => [
                            'category' => $userPreferences['categories'],
                            'boost' => 2.0
                        ]],
                        // ์‚ฌ์šฉ์ž์˜ ๊ฐ€๊ฒฉ๋Œ€์— ๋งž๋Š” ์ƒํ’ˆ
                        ['range' => [
                            'price' => [
                                'gte' => $userPreferences['price_min'],
                                'lte' => $userPreferences['price_max'],
                                'boost' => 1.5
                            ]
                        ]]
                    ]
                ]
            ]
        ]
    ];
    
    return $client->search($params);
}

function getUserPreferences($userId) {
    // ์‹ค์ œ๋กœ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ฐ€์ ธ์™€์•ผ ํ•จ
    return [
        'categories' => ['์ „์ž์ œํ’ˆ', '์ปดํ“จํ„ฐ์ฃผ๋ณ€๊ธฐ๊ธฐ'],
        'price_min' => 50000,
        'price_max' => 200000
    ];
}

๐Ÿ“Š ๊ฒ€์ƒ‰ ๋ถ„์„ (Search Analytics)

์–ด๋–ค ๊ฒ€์ƒ‰์–ด๊ฐ€ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š”์ง€, ๊ฒฐ๊ณผ๊ฐ€ ์—†๋Š” ๊ฒ€์ƒ‰์€ ๋ฌด์—‡์ธ์ง€ ๋ถ„์„ํ•˜๋ฉด ์„œ๋น„์Šค ๊ฐœ์„ ์— ๋„์›€์ด ๋ผ!

<?php
class SearchAnalytics {
    private $pdo;
    
    public function logSearch($keyword, $resultCount, $userId = null) {
        $stmt = $this->pdo->prepare("
            INSERT INTO search_logs 
            (keyword, result_count, user_id, created_at) 
            VALUES (?, ?, ?, NOW())
        ");
        $stmt->execute([$keyword, $resultCount, $userId]);
    }
    
    public function getPopularSearches($limit = 10) {
        $stmt = $this->pdo->prepare("
            SELECT keyword, COUNT(*) as count 
            FROM search_logs 
            WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
            GROUP BY keyword 
            ORDER BY count DESC 
            LIMIT ?
        ");
        $stmt->execute([$limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    
    public function getZeroResultSearches($limit = 10) {
        $stmt = $this->pdo->prepare("
            SELECT keyword, COUNT(*) as count 
            FROM search_logs 
            WHERE result_count = 0 
            AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
            GROUP BY keyword 
            ORDER BY count DESC 
            LIMIT ?
        ");
        $stmt->execute([$limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$analytics = new SearchAnalytics($pdo);

// ๊ฒ€์ƒ‰ ์‹œ ๋กœ๊น…
$response = $client->search($params);
$analytics->logSearch(
    $keyword, 
    $response['hits']['total']['value'],
    $currentUserId
);

// ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด ํ™•์ธ
$popular = $analytics->getPopularSearches();
foreach ($popular as $item) {
    echo "{$item['keyword']}: {$item['count']}ํšŒ\n";
}

// ๊ฒฐ๊ณผ ์—†๋Š” ๊ฒ€์ƒ‰์–ด ํ™•์ธ (์ƒํ’ˆ ์ถ”๊ฐ€ ํ•„์š”)
$zeroResults = $analytics->getZeroResultSearches();
foreach ($zeroResults as $item) {
    echo "{$item['keyword']}: {$item['count']}ํšŒ ๊ฒ€์ƒ‰๋จ\n";
}

๐Ÿ”„ ๋™์˜์–ด ์ฒ˜๋ฆฌ (Synonyms)

"๋…ธํŠธ๋ถ"์„ ๊ฒ€์ƒ‰ํ•ด๋„ "๋žฉํƒ‘"์ด ๋‚˜์˜ค๊ฒŒ ํ•˜๋ ค๋ฉด ๋™์˜์–ด ์„ค์ •์ด ํ•„์š”ํ•ด!

<?php
// ๋™์˜์–ด๊ฐ€ ํฌํ•จ๋œ ์ธ๋ฑ์Šค ์ƒ์„ฑ
$params = [
    'index' => 'products',
    'body' => [
        'settings' => [
            'analysis' => [
                'filter' => [
                    'synonym_filter' => [
                        'type' => 'synonym',
                        'synonyms' => [
                            '๋…ธํŠธ๋ถ, ๋žฉํƒ‘, ๋…ธํŠธ๋ถ์ปดํ“จํ„ฐ',
                            '์Šค๋งˆํŠธํฐ, ํœด๋Œ€ํฐ, ํ•ธ๋“œํฐ',
                            '์ด์–ดํฐ, ์ด์–ด๋ฒ„๋“œ, ์ด์–ด์…‹',
                            '๋ฌด์„ , ์™€์ด์–ด๋ฆฌ์Šค',
                            '๋ธ”๋ฃจํˆฌ์Šค, BT'
                        ]
                    ]
                ],
                'analyzer' => [
                    'synonym_analyzer' => [
                        'type' => 'custom',
                        'tokenizer' => 'standard',
                        'filter' => ['lowercase', 'synonym_filter']
                    ]
                ]
            ]
        ],
        'mappings' => [
            'properties' => [
                'name' => [
                    'type' => 'text',
                    'analyzer' => 'synonym_analyzer'
                ]
            ]
        ]
    ]
];

$client->indices()->create($params);

์ด์ œ "๋žฉํƒ‘"์„ ๊ฒ€์ƒ‰ํ•ด๋„ "๋…ธํŠธ๋ถ"์ด ํฌํ•จ๋œ ์ƒํ’ˆ์ด ๋‚˜์™€! ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ›จ์”ฌ ์ข‹์•„์ง€์ง€? ๐Ÿ˜Š

๐ŸŽ“ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ์žฌ๋Šฅ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ด์„œ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์žฌ๋Šฅ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์ž!

Step 1: ์žฌ๋Šฅ ์ธ๋ฑ์Šค ์„ค๊ณ„

<?php
$params = [
    'index' => 'talents',
    'body' => [
        'settings' => [
            'number_of_shards' => 2,
            'number_of_replicas' => 1,
            'analysis' => [
                'analyzer' => [
                    'korean' => [
                        'type' => 'custom',
                        'tokenizer' => 'nori_tokenizer',
                        'filter' => ['lowercase', 'nori_part_of_speech']
                    ]
                ]
            ]
        ],
        'mappings' => [
            'properties' => [
                'id' => ['type' => 'integer'],
                'title' => [
                    'type' => 'text',
                    'analyzer' => 'korean',
                    'fields' => [
                        'keyword' => ['type' => 'keyword'],
                        'suggest' => ['type' => 'completion']
                    ]
                ],
                'description' => [
                    'type' => 'text',
                    'analyzer' => 'korean'
                ],
                'category' => ['type' => 'keyword'],
                'subcategory' => ['type' => 'keyword'],
                'tags' => ['type' => 'keyword'],
                'seller_id' => ['type' => 'integer'],
                'seller_name' => [
                    'type' => 'text',
                    'fields' => ['keyword' => ['type' => 'keyword']]
                ],
                'price' => ['type' => 'integer'],
                'rating' => ['type' => 'float'],
                'review_count' => ['type' => 'integer'],
                'order_count' => ['type' => 'integer'],
                'delivery_days' => ['type' => 'integer'],
                'location' => ['type' => 'geo_point'],
                'is_online' => ['type' => 'boolean'],
                'is_express' => ['type' => 'boolean'],
                'created_at' => ['type' => 'date'],
                'updated_at' => ['type' => 'date']
            ]
        ]
    ]
];

$client->indices()->create($params);

Step 2: ์žฌ๋Šฅ ๊ฒ€์ƒ‰ ํด๋ž˜์Šค

<?php
class TalentSearch {
    private $client;
    private $index = 'talents';
    
    public function __construct($client) {
        $this->client = $client;
    }
    
    /**
     * ์žฌ๋Šฅ ๊ฒ€์ƒ‰
     */
    public function search($options = []) {
        $keyword = $options['keyword'] ?? '';
        $category = $options['category'] ?? '';
        $priceMin = $options['price_min'] ?? null;
        $priceMax = $options['price_max'] ?? null;
        $ratingMin = $options['rating_min'] ?? null;
        $isOnline = $options['is_online'] ?? null;
        $isExpress = $options['is_express'] ?? null;
        $location = $options['location'] ?? null;
        $distance = $options['distance'] ?? 10; // km
        $sort = $options['sort'] ?? 'relevance';
        $page = max(1, $options['page'] ?? 1);
        $perPage = min(100, max(1, $options['per_page'] ?? 20));
        
        // ์ฟผ๋ฆฌ ๊ตฌ์„ฑ
        $must = [];
        $filter = [];
        $should = [];
        
        // ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰
        if (!empty($keyword)) {
            $must[] = [
                'multi_match' => [
                    'query' => $keyword,
                    'fields' => [
                        'title^3',
                        'description^2',
                        'tags^2',
                        'seller_name',
                        'category'
                    ],
                    'type' => 'best_fields',
                    'fuzziness' => 'AUTO'
                ]
            ];
        }
        
        // ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ
        if (!empty($category)) {
            $filter[] = ['term' => ['category' => $category]];
        }
        
        // ๊ฐ€๊ฒฉ ๋ฒ”์œ„
        if ($priceMin !== null || $priceMax !== null) {
            $range = [];
            if ($priceMin !== null) $range['gte'] = $priceMin;
            if ($priceMax !== null) $range['lte'] = $priceMax;
            $filter[] = ['range' => ['price' => $range]];
        }
        
        // ํ‰์  ํ•„ํ„ฐ
        if ($ratingMin !== null) {
            $filter[] = ['range' => ['rating' => ['gte' => $ratingMin]]];
        }
        
        // ์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ
        if ($isOnline !== null) {
            $filter[] = ['term' => ['is_online' => $isOnline]];
        }
        
        // ๋น ๋ฅธ ๋ฐฐ์†ก
        if ($isExpress) {
            $filter[] = ['term' => ['is_express' => true]];
        }
        
        // ์œ„์น˜ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰
        if ($location && !empty($location['lat']) && !empty($location['lon'])) {
            $filter[] = [
                'geo_distance' => [
                    'distance' => $distance . 'km',
                    'location' => [
                        'lat' => $location['lat'],
                        'lon' => $location['lon']
                    ]
                ]
            ];
        }
        
        // ์ธ๊ธฐ ํŒ๋งค์ž ๋ถ€์ŠคํŠธ
        $should[] = [
            'range' => [
                'order_count' => [
                    'gte' => 100,
                    'boost' => 1.5
                ]
            ]
        ];
        
        // ๋†’์€ ํ‰์  ๋ถ€์ŠคํŠธ
        $should[] = [
            'range' => [
                'rating' => [
                    'gte' => 4.5,
                    'boost' => 1.3
                ]
            ]
        ];
        
        // Bool ์ฟผ๋ฆฌ
        $query = ['bool' => []];
        if (!empty($must)) $query['bool']['must'] = $must;
        if (!empty($filter)) $query['bool']['filter'] = $filter;
        if (!empty($should)) {
            $query['bool']['should'] = $should;
            $query['bool']['minimum_should_match'] = 0;
        }
        
        // ์ „์ฒด ๊ฒ€์ƒ‰
        if (empty($must) && empty($filter)) {
            $query = ['match_all' => new \stdClass()];
        }
        
        // ์ •๋ ฌ
        $sortOptions = $this->buildSort($sort, $location);
        
        // ๊ฒ€์ƒ‰ ์‹คํ–‰
        $params = [
            'index' => $this->index,
            'body' => [
                'query' => $query,
                'from' => ($page - 1) * $perPage,
                'size' => $perPage,
                'sort' => $sortOptions,
                'highlight' => [
                    'fields' => [
                        'title' => new \stdClass(),
                        'description' => [
                            'fragment_size' => 150,
                            'number_of_fragments' => 2
                        ]
                    ]
                ],
                'aggs' => [
                    'categories' => [
                        'terms' => ['field' => 'category', 'size' => 20]
                    ],
                    'price_ranges' => [
                        'range' => [
                            'field' => 'price',
                            'ranges' => [
                                ['key' => '1๋งŒ์› ๋ฏธ๋งŒ', 'to' => 10000],
                                ['key' => '1๋งŒ์›~5๋งŒ์›', 'from' => 10000, 'to' => 50000],
                                ['key' => '5๋งŒ์›~10๋งŒ์›', 'from' => 50000, 'to' => 100000],
                                ['key' => '10๋งŒ์› ์ด์ƒ', 'from' => 100000]
                            ]
                        ]
                    ],
                    'avg_price' => ['avg' => ['field' => 'price']],
                    'avg_rating' => ['avg' => ['field' => 'rating']]
                ]
            ]
        ];
        
        return $this->client->search($params);
    }
    
    /**
     * ์ •๋ ฌ ์˜ต์…˜
     */
    private function buildSort($sort, $location = null) {
        switch ($sort) {
            case 'price_asc':
                return [['price' => ['order' => 'asc']]];
            case 'price_desc':
                return [['price' => ['order' => 'desc']]];
            case 'rating':
                return [
                    ['rating' => ['order' => 'desc']],
                    ['review_count' => ['order' => 'desc']]
                ];
            case 'popular':
                return [['order_count' => ['order' => 'desc']]];
            case 'newest':
                return [['created_at' => ['order' => 'desc']]];
            case 'distance':
                if ($location && !empty($location['lat']) && !empty($location['lon'])) {
                    return [[
                        '_geo_distance' => [
                            'location' => [
                                'lat' => $location['lat'],
                                'lon' => $location['lon']
                            ],
                            'order' => 'asc',
                            'unit' => 'km'
                        ]
                    ]];
                }
                // fallthrough
            case 'relevance':
            default:
                return [['_score' => ['order' => 'desc']]];
        }
    }
    
    /**
     * ์ž๋™์™„์„ฑ
     */
    public function autocomplete($prefix) {
        $params = [
            'index' => $this->index,
            'body' => [
                'suggest' => [
                    'talent-suggest' => [
                        'prefix' => $prefix,
                        'completion' => [
                            'field' => 'title.suggest',
                            'size' => 10,
                            'skip_duplicates' => true,
                            'fuzzy' => ['fuzziness' => 1]
                        ]
                    ]
                ]
            ]
        ];
        
        $response = $this->client->search($params);
        $suggestions = [];
        
        foreach ($response['suggest']['talent-suggest'][0]['options'] as $option) {
            $suggestions[] = [
                'text' => $option['text'],
                'score' => $option['_score']
            ];
        }
        
        return $suggestions;
    }
    
    /**
     * ์œ ์‚ฌ ์žฌ๋Šฅ ์ถ”์ฒœ
     */
    public function moreLikeThis($talentId) {
        $params = [
            'index' => $this->index,
            'body' => [
                'query' => [
                    'more_like_this' => [
                        'fields' => ['title', 'description', 'tags'],
                        'like' => [
                            [
                                '_index' => $this->index,
                                '_id' => $talentId
                            ]
                        ],
                        'min_term_freq' => 1,
                        'max_query_terms' => 12
                    ]
                ],
                'size' => 10
            ]
        ];
        
        return $this->client->search($params);
    }
}

Step 3: API ์—”๋“œํฌ์ธํŠธ

<?php
// talent_search_api.php
require 'vendor/autoload.php';
require 'TalentSearch.php';

header('Content-Type: application/json; charset=utf-8');

$client = ClientBuilder::create()
    ->setHosts(['localhost:9200'])
    ->build();

$search = new TalentSearch($client);

$action = $_GET['action'] ?? 'search';

try {
    switch ($action) {
        case 'search':
            $options = [
                'keyword' => $_GET['q'] ?? '',
                'category' => $_GET['category'] ?? '',
                'price_min' => isset($_GET['price_min']) ? intval($_GET['price_min']) : null,
                'price_max' => isset($_GET['price_max']) ? intval($_GET['price_max']) : null,
                'rating_min' => isset($_GET['rating_min']) ? floatval($_GET['rating_min']) : null,
                'is_online' => isset($_GET['is_online']) ? ($_GET['is_online'] === '1') : null,
                'is_express' => isset($_GET['is_express']) ? ($_GET['is_express'] === '1') : null,
                'sort' => $_GET['sort'] ?? 'relevance',
                'page' => intval($_GET['page'] ?? 1),
                'per_page' => intval($_GET['per_page'] ?? 20)
            ];
            
            // ์œ„์น˜ ์ •๋ณด
            if (!empty($_GET['lat']) && !empty($_GET['lon'])) {
                $options['location'] = [
                    'lat' => floatval($_GET['lat']),
                    'lon' => floatval($_GET['lon'])
                ];
                $options['distance'] = intval($_GET['distance'] ?? 10);
            }
            
            $response = $search->search($options);
            
            $result = [
                'success' => true,
                'total' => $response['hits']['total']['value'],
                'took' => $response['took'],
                'items' => [],
                'aggregations' => $response['aggregations'] ?? []
            ];
            
            foreach ($response['hits']['hits'] as $hit) {
                $item = $hit['_source'];
                $item['score'] = $hit['_score'];
                
                if (isset($hit['highlight'])) {
                    $item['highlight'] = $hit['highlight'];
                }
                
                if (isset($hit['sort'])) {
                    $item['distance'] = $hit['sort'][0] ?? null;
                }
                
                $result['items'][] = $item;
            }
            
            echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
            break;
            
        case 'autocomplete':
            $prefix = $_GET['q'] ?? '';
            $suggestions = $search->autocomplete($prefix);
            
            echo json_encode([
                'success' => true,
                'suggestions' => $suggestions
            ], JSON_UNESCAPED_UNICODE);
            break;
            
        case 'similar':
            $talentId = $_GET['id'] ?? '';
            $response = $search->moreLikeThis($talentId);
            
            $items = [];
            foreach ($response['hits']['hits'] as $hit) {
                $items[] = $hit['_source'];
            }
            
            echo json_encode([
                'success' => true,
                'items' => $items
            ], JSON_UNESCAPED_UNICODE);
            break;
            
        default:
            throw new Exception('Invalid action');
    }
    
} catch (Exception $e) {
    http_response_code(500);
    echo json_encode([
        'success' => false,
        'error' => $e->getMessage()
    ], JSON_UNESCAPED_UNICODE);
}

๐Ÿ’ญ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด. PHP์™€ Elasticsearch๋ฅผ ํ™œ์šฉํ•œ ์ „๋ฌธ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ ๊ตฌ์ถ•์— ๋Œ€ํ•ด ์•Œ์•„๋ดค๋Š”๋ฐ, ์–ด๋•Œ? ์ƒ๊ฐ๋ณด๋‹ค ๋ณต์žกํ•˜์ง€๋งŒ ๋ง‰์ƒ ํ•ด๋ณด๋ฉด ์žฌ๋ฏธ์žˆ์ง€? ๐Ÿ˜Š

๐ŸŽฏ ํ•ต์‹ฌ ์š”์•ฝ

1. Elasticsearch์˜ ๊ฐ•์ 
โ€ข ๋น ๋ฅธ ๊ฒ€์ƒ‰ ์†๋„ (์—ญ์ƒ‰์ธ ๊ตฌ์กฐ)
โ€ข ์˜คํƒ€ ํ—ˆ์šฉ (ํผ์ง€ ๊ฒ€์ƒ‰)
โ€ข ๊ด€๋ จ์„ฑ ๊ธฐ๋ฐ˜ ์ •๋ ฌ (์Šค์ฝ”์–ด๋ง)
โ€ข ๊ฐ•๋ ฅํ•œ ์ง‘๊ณ„ ๊ธฐ๋Šฅ
โ€ข ํ™•์žฅ์„ฑ (์ƒค๋”ฉ, ๋ ˆํ”Œ๋ฆฌ์นด)

2. ๊ฒ€์ƒ‰ ํ’ˆ์งˆ ํ–ฅ์ƒ ๋ฐฉ๋ฒ•
โ€ข ์ ์ ˆํ•œ ๋งคํ•‘ ์„ค๊ณ„
โ€ข ํ•œ๊ตญ์–ด ํ˜•ํƒœ์†Œ ๋ถ„์„๊ธฐ ํ™œ์šฉ
โ€ข ๋™์˜์–ด ์ฒ˜๋ฆฌ
โ€ข ๊ฐœ์ธํ™” ๊ฒ€์ƒ‰
โ€ข ํ•˜์ด๋ผ์ดํŒ…

3. ์„ฑ๋Šฅ ์ตœ์ ํ™”
โ€ข Bulk API ์‚ฌ์šฉ
โ€ข Filter context ํ™œ์šฉ
โ€ข ์ ์ ˆํ•œ ์ƒค๋“œ ์„ค์ •
โ€ข ์ฟผ๋ฆฌ ์บ์‹ฑ
โ€ข ๋ชจ๋‹ˆํ„ฐ๋ง

4. ๋ณด์•ˆ๊ณผ ์šด์˜
โ€ข ์ธ์ฆ ์„ค์ •
โ€ข ์—ญํ•  ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด
โ€ข ์ •๊ธฐ ๋ฐฑ์—…
โ€ข ๋กœ๊ทธ ๋ถ„์„

๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ

์ด์ œ ๊ธฐ๋ณธ์€ ๋งˆ์Šคํ„ฐํ–ˆ์œผ๋‹ˆ ๋” ๋‚˜์•„๊ฐ€๋ณด์ž!

ํ•™์Šต ์ถ”์ฒœ:
โ€ข Logstash๋กœ ๋ฐ์ดํ„ฐ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•
โ€ข Kibana๋กœ ์‹œ๊ฐํ™” ๋Œ€์‹œ๋ณด๋“œ ๋งŒ๋“ค๊ธฐ
โ€ข ๋จธ์‹ ๋Ÿฌ๋‹ ๊ธฐ๋Šฅ ํ™œ์šฉ (์ด์ƒ ํƒ์ง€, ์ถ”์ฒœ)
โ€ข ํด๋Ÿฌ์Šคํ„ฐ ๊ตฌ์„ฑ๊ณผ ์ƒค๋“œ ๊ด€๋ฆฌ
โ€ข ์„ฑ๋Šฅ ํŠœ๋‹ ์‹ฌํ™”

์‹ค์ „ ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด:
โ€ข ์ด์ปค๋จธ์Šค ์ƒํ’ˆ ๊ฒ€์ƒ‰
โ€ข ์ฑ„์šฉ ๊ณต๊ณ  ๊ฒ€์ƒ‰
โ€ข ๋ถ€๋™์‚ฐ ๋งค๋ฌผ ๊ฒ€์ƒ‰
โ€ข ๋ธ”๋กœ๊ทธ/๋‰ด์Šค ๊ฒ€์ƒ‰
โ€ข ์žฌ๋Šฅ ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค (์žฌ๋Šฅ๋„ท์ฒ˜๋Ÿผ!)

โœจ ๋งˆ์ง€๋ง‰ ์กฐ์–ธ

๊ฒ€์ƒ‰์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์˜ ํ•ต์‹ฌ์ด์•ผ. ๋น ๋ฅด๊ณ  ์ •ํ™•ํ•œ ๊ฒ€์ƒ‰์€ ์‚ฌ์šฉ์ž๋ฅผ ๋งŒ์กฑ์‹œํ‚ค๊ณ , ๋น„์ฆˆ๋‹ˆ์Šค ์„ฑ๊ณผ๋กœ ์ด์–ด์ ธ. Elasticsearch๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ง€๋งŒ, ์ œ๋Œ€๋กœ ํ™œ์šฉํ•˜๋ ค๋ฉด ์ง€์†์ ์ธ ํ•™์Šต๊ณผ ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ•ด.

์ฒ˜์Œ์—” ์–ด๋ ต๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ ์šฉํ•ด๋ณด๋ฉด ๊ธˆ๋ฐฉ ์ต์ˆ™ํ•ด์งˆ ๊ฑฐ์•ผ. ์ž‘์€ ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์  ํ™•์žฅํ•ด๋‚˜๊ฐ€๋Š” ๊ฑธ ์ถ”์ฒœํ•ด!

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์„ ๋งŒ๋“ค๊ณ  ์‹ถ๋‹ค๋ฉด, ์ด ๊ธ€์—์„œ ๋ฐฐ์šด ๋‚ด์šฉ์ด ํฐ ๋„์›€์ด ๋  ๊ฑฐ์•ผ. ์‚ฌ์šฉ์ž๋“ค์ด ์›ํ•˜๋Š” ์žฌ๋Šฅ์„ ๋น ๋ฅด๊ณ  ์ •ํ™•ํ•˜๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ, ์ •๋ง ๋ฉ‹์ง€์ง€ ์•Š์•„? ๐ŸŽจ

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด Elasticsearch ๊ณต์‹ ๋ฌธ์„œ์™€ ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ํ™œ์šฉํ•ด๋ด. ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ๋น„์Šทํ•œ ๋ฌธ์ œ๋ฅผ ๊ฒช๊ณ  ํ•ด๊ฒฐ์ฑ…์„ ๊ณต์œ ํ•˜๊ณ  ์žˆ๊ฑฐ๋“ !

์ž, ์ด์ œ ๋‹น์‹ ์˜ ํ”„๋กœ์ ํŠธ์— ๊ฐ•๋ ฅํ•œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณผ ์‹œ๊ฐ„์ด์•ผ. ํ™”์ดํŒ…! ๐Ÿ’ช

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.