[{"data":1,"prerenderedAt":1465},["ShallowReactive",2],{"post-laravel-mysql-fulltext-search":3},{"id":4,"title":5,"body":6,"date":1453,"description":1454,"extension":1455,"meta":1456,"navigation":128,"path":1461,"seo":1462,"stem":1463,"__hash__":1464},"blog\u002Fblog\u002Flaravel-mysql-fulltext-search.md","LaravelでMySQL全文検索を実装する",{"type":7,"value":8,"toc":1428},"minimark",[9,14,22,45,48,52,60,63,71,74,77,82,109,113,204,208,211,476,479,484,493,512,516,525,536,540,570,578,582,678,704,708,861,864,868,877,897,901,944,948,1233,1236,1239,1249,1278,1281,1301,1305,1309,1324,1338,1341,1356,1368,1371,1374,1394,1397,1400,1424],[10,11,13],"h2",{"id":12},"like検索の課題","LIKE検索の課題",[15,16,17,21],"p",{},[18,19,20],"code",{},"LIKE '%keyword%'"," を使った部分一致検索は、データ量が増えるとパフォーマンスが低下します。",[23,24,29],"pre",{"className":25,"code":26,"language":27,"meta":28,"style":28},"language-sql shiki shiki-themes github-light github-dark","-- インデックスが使えない\nSELECT * FROM shops WHERE name LIKE '%太郎%'\n","sql","",[18,30,31,39],{"__ignoreMap":28},[32,33,36],"span",{"class":34,"line":35},"line",1,[32,37,38],{},"-- インデックスが使えない\n",[32,40,42],{"class":34,"line":41},2,[32,43,44],{},"SELECT * FROM shops WHERE name LIKE '%太郎%'\n",[15,46,47],{},"MySQLの**全文検索インデックス（Fulltext Index）**を使うことで、この問題を解決できます。",[10,49,51],{"id":50},"n-gramパーサーとは","N-gramパーサーとは",[15,53,54,55,59],{},"MySQLの全文検索では、",[56,57,58],"strong",{},"N-gramパーサー","を使用することで日本語にも対応できます。",[15,61,62],{},"N-gramは文字列をN文字ずつに分割してインデックスを作成する方法です。",[23,64,69],{"className":65,"code":67,"language":68},[66],"language-text","\"サンプル太郎\" → [\"サン\", \"ンプ\", \"プル\", \"ル太\", \"太郎\"]（2-gram\u002FBigram）\n","text",[18,70,67],{"__ignoreMap":28},[15,72,73],{},"これにより、部分一致検索でもインデックスを活用できます。",[10,75,76],{"id":76},"実装手順",[78,79,81],"h3",{"id":80},"_1-modelとmigrationを生成","1. Modelとmigrationを生成",[23,83,87],{"className":84,"code":85,"language":86,"meta":28,"style":28},"language-bash shiki shiki-themes github-light github-dark","php artisan make:model Shop --migration\n","bash",[18,88,89],{"__ignoreMap":28},[32,90,91,95,99,102,105],{"class":34,"line":35},[32,92,94],{"class":93},"sScJk","php",[32,96,98],{"class":97},"sZZnC"," artisan",[32,100,101],{"class":97}," make:model",[32,103,104],{"class":97}," Shop",[32,106,108],{"class":107},"sj4cs"," --migration\n",[78,110,112],{"id":111},"_2-modelを定義","2. Modelを定義",[23,114,117],{"className":115,"code":116,"language":94,"meta":28,"style":28},"language-php shiki shiki-themes github-light github-dark","\u003C?php\n\nnamespace App\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass Shop extends Model\n{\n    use HasFactory;\n\n    protected $table = 'shops';\n\n    protected $fillable = ['name', 'age', 'gender_id'];\n}\n",[18,118,119,124,130,136,141,147,153,158,164,170,176,181,187,192,198],{"__ignoreMap":28},[32,120,121],{"class":34,"line":35},[32,122,123],{},"\u003C?php\n",[32,125,126],{"class":34,"line":41},[32,127,129],{"emptyLinePlaceholder":128},true,"\n",[32,131,133],{"class":34,"line":132},3,[32,134,135],{},"namespace App\\Models;\n",[32,137,139],{"class":34,"line":138},4,[32,140,129],{"emptyLinePlaceholder":128},[32,142,144],{"class":34,"line":143},5,[32,145,146],{},"use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\n",[32,148,150],{"class":34,"line":149},6,[32,151,152],{},"use Illuminate\\Database\\Eloquent\\Model;\n",[32,154,156],{"class":34,"line":155},7,[32,157,129],{"emptyLinePlaceholder":128},[32,159,161],{"class":34,"line":160},8,[32,162,163],{},"class Shop extends Model\n",[32,165,167],{"class":34,"line":166},9,[32,168,169],{},"{\n",[32,171,173],{"class":34,"line":172},10,[32,174,175],{},"    use HasFactory;\n",[32,177,179],{"class":34,"line":178},11,[32,180,129],{"emptyLinePlaceholder":128},[32,182,184],{"class":34,"line":183},12,[32,185,186],{},"    protected $table = 'shops';\n",[32,188,190],{"class":34,"line":189},13,[32,191,129],{"emptyLinePlaceholder":128},[32,193,195],{"class":34,"line":194},14,[32,196,197],{},"    protected $fillable = ['name', 'age', 'gender_id'];\n",[32,199,201],{"class":34,"line":200},15,[32,202,203],{},"}\n",[78,205,207],{"id":206},"_3-全文検索用のmigrationを作成","3. 全文検索用のmigrationを作成",[15,209,210],{},"仮想カラムと全文検索インデックスを定義します。",[23,212,214],{"className":115,"code":213,"language":94,"meta":28,"style":28},"\u003C?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Schema;\n\nclass CreateShopsTable extends Migration\n{\n    public function up()\n    {\n        Schema::create('shops', function (Blueprint $table) {\n            $table->bigIncrements('id')->comment('店舗ID');\n            $table->string('name', 255)->comment('店舗名');\n            $table->unsignedInteger('age')->comment('設立年数');\n            $table->smallInteger('gender_id')->comment('対象性別');\n            $table->timestamps();\n        });\n\n        \u002F\u002F 検索用の仮想カラムを追加\n        DB::statement(\"\n            ALTER TABLE shops\n            ADD free_word TEXT AS (\n                CONCAT(\n                    IFNULL(age, ''), ' ',\n                    IFNULL(name, ''), ' ',\n                    CASE gender_id\n                        WHEN 1 THEN '男性'\n                        WHEN 2 THEN '女性'\n                        ELSE ''\n                    END\n                )\n            ) STORED\n        \");\n\n        \u002F\u002F N-gram全文検索インデックスを作成\n        DB::statement(\"\n            ALTER TABLE shops\n            ADD FULLTEXT INDEX ftx_free_word (free_word)\n            WITH PARSER ngram\n        \");\n    }\n\n    public function down()\n    {\n        Schema::dropIfExists('shops');\n    }\n}\n",[18,215,216,220,224,229,234,239,244,248,253,257,262,267,272,277,282,287,293,299,305,310,316,322,328,334,340,346,352,358,364,370,376,382,388,394,400,405,411,416,421,427,433,438,444,449,455,460,466,471],{"__ignoreMap":28},[32,217,218],{"class":34,"line":35},[32,219,123],{},[32,221,222],{"class":34,"line":41},[32,223,129],{"emptyLinePlaceholder":128},[32,225,226],{"class":34,"line":132},[32,227,228],{},"use Illuminate\\Database\\Migrations\\Migration;\n",[32,230,231],{"class":34,"line":138},[32,232,233],{},"use Illuminate\\Database\\Schema\\Blueprint;\n",[32,235,236],{"class":34,"line":143},[32,237,238],{},"use Illuminate\\Support\\Facades\\DB;\n",[32,240,241],{"class":34,"line":149},[32,242,243],{},"use Illuminate\\Support\\Facades\\Schema;\n",[32,245,246],{"class":34,"line":155},[32,247,129],{"emptyLinePlaceholder":128},[32,249,250],{"class":34,"line":160},[32,251,252],{},"class CreateShopsTable extends Migration\n",[32,254,255],{"class":34,"line":166},[32,256,169],{},[32,258,259],{"class":34,"line":172},[32,260,261],{},"    public function up()\n",[32,263,264],{"class":34,"line":178},[32,265,266],{},"    {\n",[32,268,269],{"class":34,"line":183},[32,270,271],{},"        Schema::create('shops', function (Blueprint $table) {\n",[32,273,274],{"class":34,"line":189},[32,275,276],{},"            $table->bigIncrements('id')->comment('店舗ID');\n",[32,278,279],{"class":34,"line":194},[32,280,281],{},"            $table->string('name', 255)->comment('店舗名');\n",[32,283,284],{"class":34,"line":200},[32,285,286],{},"            $table->unsignedInteger('age')->comment('設立年数');\n",[32,288,290],{"class":34,"line":289},16,[32,291,292],{},"            $table->smallInteger('gender_id')->comment('対象性別');\n",[32,294,296],{"class":34,"line":295},17,[32,297,298],{},"            $table->timestamps();\n",[32,300,302],{"class":34,"line":301},18,[32,303,304],{},"        });\n",[32,306,308],{"class":34,"line":307},19,[32,309,129],{"emptyLinePlaceholder":128},[32,311,313],{"class":34,"line":312},20,[32,314,315],{},"        \u002F\u002F 検索用の仮想カラムを追加\n",[32,317,319],{"class":34,"line":318},21,[32,320,321],{},"        DB::statement(\"\n",[32,323,325],{"class":34,"line":324},22,[32,326,327],{},"            ALTER TABLE shops\n",[32,329,331],{"class":34,"line":330},23,[32,332,333],{},"            ADD free_word TEXT AS (\n",[32,335,337],{"class":34,"line":336},24,[32,338,339],{},"                CONCAT(\n",[32,341,343],{"class":34,"line":342},25,[32,344,345],{},"                    IFNULL(age, ''), ' ',\n",[32,347,349],{"class":34,"line":348},26,[32,350,351],{},"                    IFNULL(name, ''), ' ',\n",[32,353,355],{"class":34,"line":354},27,[32,356,357],{},"                    CASE gender_id\n",[32,359,361],{"class":34,"line":360},28,[32,362,363],{},"                        WHEN 1 THEN '男性'\n",[32,365,367],{"class":34,"line":366},29,[32,368,369],{},"                        WHEN 2 THEN '女性'\n",[32,371,373],{"class":34,"line":372},30,[32,374,375],{},"                        ELSE ''\n",[32,377,379],{"class":34,"line":378},31,[32,380,381],{},"                    END\n",[32,383,385],{"class":34,"line":384},32,[32,386,387],{},"                )\n",[32,389,391],{"class":34,"line":390},33,[32,392,393],{},"            ) STORED\n",[32,395,397],{"class":34,"line":396},34,[32,398,399],{},"        \");\n",[32,401,403],{"class":34,"line":402},35,[32,404,129],{"emptyLinePlaceholder":128},[32,406,408],{"class":34,"line":407},36,[32,409,410],{},"        \u002F\u002F N-gram全文検索インデックスを作成\n",[32,412,414],{"class":34,"line":413},37,[32,415,321],{},[32,417,419],{"class":34,"line":418},38,[32,420,327],{},[32,422,424],{"class":34,"line":423},39,[32,425,426],{},"            ADD FULLTEXT INDEX ftx_free_word (free_word)\n",[32,428,430],{"class":34,"line":429},40,[32,431,432],{},"            WITH PARSER ngram\n",[32,434,436],{"class":34,"line":435},41,[32,437,399],{},[32,439,441],{"class":34,"line":440},42,[32,442,443],{},"    }\n",[32,445,447],{"class":34,"line":446},43,[32,448,129],{"emptyLinePlaceholder":128},[32,450,452],{"class":34,"line":451},44,[32,453,454],{},"    public function down()\n",[32,456,458],{"class":34,"line":457},45,[32,459,266],{},[32,461,463],{"class":34,"line":462},46,[32,464,465],{},"        Schema::dropIfExists('shops');\n",[32,467,469],{"class":34,"line":468},47,[32,470,443],{},[32,472,474],{"class":34,"line":473},48,[32,475,203],{},[78,477,478],{"id":478},"migrationの解説",[480,481,483],"h4",{"id":482},"仮想カラムgenerated-column","仮想カラム（Generated Column）",[23,485,487],{"className":25,"code":486,"language":27,"meta":28,"style":28},"ALTER TABLE shops ADD free_word TEXT AS (...) STORED\n",[18,488,489],{"__ignoreMap":28},[32,490,491],{"class":34,"line":35},[32,492,486],{},[494,495,496,503,509],"ul",{},[497,498,499,502],"li",{},[56,500,501],{},"仮想カラム"," 他のカラムから自動生成されるカラム",[497,504,505,508],{},[56,506,507],{},"STORED"," 物理的に保存される（検索インデックスを作成可能）",[497,510,511],{},"複数のカラムを結合して検索対象を作成",[480,513,515],{"id":514},"concat関数","CONCAT関数",[23,517,519],{"className":25,"code":518,"language":27,"meta":28,"style":28},"CONCAT(IFNULL(age, ''), ' ', IFNULL(name, ''), ' ', ...)\n",[18,520,521],{"__ignoreMap":28},[32,522,523],{"class":34,"line":35},[32,524,518],{},[494,526,527,530],{},[497,528,529],{},"複数の文字列を連結",[497,531,532,535],{},[18,533,534],{},"IFNULL",": NULL値を空文字列に変換",[480,537,539],{"id":538},"case式","CASE式",[23,541,543],{"className":25,"code":542,"language":27,"meta":28,"style":28},"CASE gender_id\n    WHEN 1 THEN '男性'\n    WHEN 2 THEN '女性'\n    ELSE ''\nEND\n",[18,544,545,550,555,560,565],{"__ignoreMap":28},[32,546,547],{"class":34,"line":35},[32,548,549],{},"CASE gender_id\n",[32,551,552],{"class":34,"line":41},[32,553,554],{},"    WHEN 1 THEN '男性'\n",[32,556,557],{"class":34,"line":132},[32,558,559],{},"    WHEN 2 THEN '女性'\n",[32,561,562],{"class":34,"line":138},[32,563,564],{},"    ELSE ''\n",[32,566,567],{"class":34,"line":143},[32,568,569],{},"END\n",[494,571,572,575],{},[497,573,574],{},"IDを日本語の名称に変換",[497,576,577],{},"日本語でも検索可能にする",[78,579,581],{"id":580},"_4-seederでテストデータを作成","4. Seederでテストデータを作成",[23,583,585],{"className":115,"code":584,"language":94,"meta":28,"style":28},"\u003C?php\n\nnamespace Database\\Seeders;\n\nuse App\\Models\\Shop;\nuse Illuminate\\Database\\Seeder;\n\nclass DummyShopsSeeder extends Seeder\n{\n    public function run()\n    {\n        $data = [\n            ['name' => 'サンプル太郎', 'age' => 25, 'gender_id' => 1],\n            ['name' => 'サンプル花子', 'age' => 30, 'gender_id' => 2],\n            ['name' => 'サンプル二郎', 'age' => 20, 'gender_id' => 1],\n        ];\n\n        Shop::query()->insert($data);\n    }\n}\n",[18,586,587,591,595,600,604,609,614,618,623,627,632,636,641,646,651,656,661,665,670,674],{"__ignoreMap":28},[32,588,589],{"class":34,"line":35},[32,590,123],{},[32,592,593],{"class":34,"line":41},[32,594,129],{"emptyLinePlaceholder":128},[32,596,597],{"class":34,"line":132},[32,598,599],{},"namespace Database\\Seeders;\n",[32,601,602],{"class":34,"line":138},[32,603,129],{"emptyLinePlaceholder":128},[32,605,606],{"class":34,"line":143},[32,607,608],{},"use App\\Models\\Shop;\n",[32,610,611],{"class":34,"line":149},[32,612,613],{},"use Illuminate\\Database\\Seeder;\n",[32,615,616],{"class":34,"line":155},[32,617,129],{"emptyLinePlaceholder":128},[32,619,620],{"class":34,"line":160},[32,621,622],{},"class DummyShopsSeeder extends Seeder\n",[32,624,625],{"class":34,"line":166},[32,626,169],{},[32,628,629],{"class":34,"line":172},[32,630,631],{},"    public function run()\n",[32,633,634],{"class":34,"line":178},[32,635,266],{},[32,637,638],{"class":34,"line":183},[32,639,640],{},"        $data = [\n",[32,642,643],{"class":34,"line":189},[32,644,645],{},"            ['name' => 'サンプル太郎', 'age' => 25, 'gender_id' => 1],\n",[32,647,648],{"class":34,"line":194},[32,649,650],{},"            ['name' => 'サンプル花子', 'age' => 30, 'gender_id' => 2],\n",[32,652,653],{"class":34,"line":200},[32,654,655],{},"            ['name' => 'サンプル二郎', 'age' => 20, 'gender_id' => 1],\n",[32,657,658],{"class":34,"line":289},[32,659,660],{},"        ];\n",[32,662,663],{"class":34,"line":295},[32,664,129],{"emptyLinePlaceholder":128},[32,666,667],{"class":34,"line":301},[32,668,669],{},"        Shop::query()->insert($data);\n",[32,671,672],{"class":34,"line":307},[32,673,443],{},[32,675,676],{"class":34,"line":312},[32,677,203],{},[23,679,681],{"className":84,"code":680,"language":86,"meta":28,"style":28},"php artisan migrate\nphp artisan db:seed --class=DummyShopsSeeder\n",[18,682,683,692],{"__ignoreMap":28},[32,684,685,687,689],{"class":34,"line":35},[32,686,94],{"class":93},[32,688,98],{"class":97},[32,690,691],{"class":97}," migrate\n",[32,693,694,696,698,701],{"class":34,"line":41},[32,695,94],{"class":93},[32,697,98],{"class":97},[32,699,700],{"class":97}," db:seed",[32,702,703],{"class":107}," --class=DummyShopsSeeder\n",[78,705,707],{"id":706},"_5-controllerで全文検索を実装","5. Controllerで全文検索を実装",[23,709,711],{"className":115,"code":710,"language":94,"meta":28,"style":28},"\u003C?php\n\nnamespace App\\Http\\Controllers;\n\nuse App\\Models\\Shop;\nuse Illuminate\\Http\\Request;\n\nclass ShopController extends Controller\n{\n    public function index(Request $request)\n    {\n        $query = Shop::query();\n        $freeWord = $request->input('free_word');\n\n        if ($freeWord) {\n            \u002F\u002F 全文検索を実行\n            $query->whereRaw(\n                \"MATCH(free_word) AGAINST (? IN BOOLEAN MODE)\",\n                [$freeWord]\n            );\n        }\n\n        $shops = $query\n            ->select(['id', 'name', 'age', 'gender_id'])\n            ->paginate(20);\n\n        return view('index', [\n            'shops' => $shops,\n            'parameters' => $request->all(),\n        ]);\n    }\n}\n",[18,712,713,717,721,726,730,734,739,743,748,752,757,761,766,771,775,780,785,790,795,800,805,810,814,819,824,829,833,838,843,848,853,857],{"__ignoreMap":28},[32,714,715],{"class":34,"line":35},[32,716,123],{},[32,718,719],{"class":34,"line":41},[32,720,129],{"emptyLinePlaceholder":128},[32,722,723],{"class":34,"line":132},[32,724,725],{},"namespace App\\Http\\Controllers;\n",[32,727,728],{"class":34,"line":138},[32,729,129],{"emptyLinePlaceholder":128},[32,731,732],{"class":34,"line":143},[32,733,608],{},[32,735,736],{"class":34,"line":149},[32,737,738],{},"use Illuminate\\Http\\Request;\n",[32,740,741],{"class":34,"line":155},[32,742,129],{"emptyLinePlaceholder":128},[32,744,745],{"class":34,"line":160},[32,746,747],{},"class ShopController extends Controller\n",[32,749,750],{"class":34,"line":166},[32,751,169],{},[32,753,754],{"class":34,"line":172},[32,755,756],{},"    public function index(Request $request)\n",[32,758,759],{"class":34,"line":178},[32,760,266],{},[32,762,763],{"class":34,"line":183},[32,764,765],{},"        $query = Shop::query();\n",[32,767,768],{"class":34,"line":189},[32,769,770],{},"        $freeWord = $request->input('free_word');\n",[32,772,773],{"class":34,"line":194},[32,774,129],{"emptyLinePlaceholder":128},[32,776,777],{"class":34,"line":200},[32,778,779],{},"        if ($freeWord) {\n",[32,781,782],{"class":34,"line":289},[32,783,784],{},"            \u002F\u002F 全文検索を実行\n",[32,786,787],{"class":34,"line":295},[32,788,789],{},"            $query->whereRaw(\n",[32,791,792],{"class":34,"line":301},[32,793,794],{},"                \"MATCH(free_word) AGAINST (? IN BOOLEAN MODE)\",\n",[32,796,797],{"class":34,"line":307},[32,798,799],{},"                [$freeWord]\n",[32,801,802],{"class":34,"line":312},[32,803,804],{},"            );\n",[32,806,807],{"class":34,"line":318},[32,808,809],{},"        }\n",[32,811,812],{"class":34,"line":324},[32,813,129],{"emptyLinePlaceholder":128},[32,815,816],{"class":34,"line":330},[32,817,818],{},"        $shops = $query\n",[32,820,821],{"class":34,"line":336},[32,822,823],{},"            ->select(['id', 'name', 'age', 'gender_id'])\n",[32,825,826],{"class":34,"line":342},[32,827,828],{},"            ->paginate(20);\n",[32,830,831],{"class":34,"line":348},[32,832,129],{"emptyLinePlaceholder":128},[32,834,835],{"class":34,"line":354},[32,836,837],{},"        return view('index', [\n",[32,839,840],{"class":34,"line":360},[32,841,842],{},"            'shops' => $shops,\n",[32,844,845],{"class":34,"line":366},[32,846,847],{},"            'parameters' => $request->all(),\n",[32,849,850],{"class":34,"line":372},[32,851,852],{},"        ]);\n",[32,854,855],{"class":34,"line":378},[32,856,443],{},[32,858,859],{"class":34,"line":384},[32,860,203],{},[78,862,863],{"id":863},"全文検索の解説",[480,865,867],{"id":866},"match-against構文","MATCH ... AGAINST構文",[23,869,871],{"className":25,"code":870,"language":27,"meta":28,"style":28},"MATCH(free_word) AGAINST ('検索キーワード' IN BOOLEAN MODE)\n",[18,872,873],{"__ignoreMap":28},[32,874,875],{"class":34,"line":35},[32,876,870],{},[494,878,879,885,891],{},[497,880,881,884],{},[56,882,883],{},"MATCH"," 全文検索インデックスを使用",[497,886,887,890],{},[56,888,889],{},"AGAINST"," 検索キーワードを指定",[497,892,893,896],{},[56,894,895],{},"BOOLEAN MODE"," 演算子を使った詳細な検索が可能",[480,898,900],{"id":899},"booleanモードの演算子","BOOLEANモードの演算子",[23,902,904],{"className":115,"code":903,"language":94,"meta":28,"style":28},"\u002F\u002F AND検索\n$query->whereRaw(\"MATCH(free_word) AGAINST ('+太郎 +男性' IN BOOLEAN MODE)\");\n\n\u002F\u002F OR検索\n$query->whereRaw(\"MATCH(free_word) AGAINST ('太郎 花子' IN BOOLEAN MODE)\");\n\n\u002F\u002F NOT検索\n$query->whereRaw(\"MATCH(free_word) AGAINST ('+サンプル -花子' IN BOOLEAN MODE)\");\n",[18,905,906,911,916,920,925,930,934,939],{"__ignoreMap":28},[32,907,908],{"class":34,"line":35},[32,909,910],{},"\u002F\u002F AND検索\n",[32,912,913],{"class":34,"line":41},[32,914,915],{},"$query->whereRaw(\"MATCH(free_word) AGAINST ('+太郎 +男性' IN BOOLEAN MODE)\");\n",[32,917,918],{"class":34,"line":132},[32,919,129],{"emptyLinePlaceholder":128},[32,921,922],{"class":34,"line":138},[32,923,924],{},"\u002F\u002F OR検索\n",[32,926,927],{"class":34,"line":143},[32,928,929],{},"$query->whereRaw(\"MATCH(free_word) AGAINST ('太郎 花子' IN BOOLEAN MODE)\");\n",[32,931,932],{"class":34,"line":149},[32,933,129],{"emptyLinePlaceholder":128},[32,935,936],{"class":34,"line":155},[32,937,938],{},"\u002F\u002F NOT検索\n",[32,940,941],{"class":34,"line":160},[32,942,943],{},"$query->whereRaw(\"MATCH(free_word) AGAINST ('+サンプル -花子' IN BOOLEAN MODE)\");\n",[78,945,947],{"id":946},"_6-viewの実装","6. Viewの実装",[23,949,951],{"className":115,"code":950,"language":94,"meta":28,"style":28},"\u003C!DOCTYPE html>\n\u003Chtml lang=\"{{ str_replace('_', '-', app()->getLocale()) }}\">\n\u003Chead>\n    \u003Cmeta charset=\"utf-8\">\n    \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    \u003Ctitle>全文検索デモ\u003C\u002Ftitle>\n    \u003Clink href=\"{{ asset('css\u002Fapp.css') }}\" rel=\"stylesheet\">\n\u003C\u002Fhead>\n\u003Cbody>\n\u003Cdiv class=\"container\">\n    \u003Cdiv class=\"card\">\n        \u003Cdiv class=\"card-header\">全文検索\u003C\u002Fdiv>\n        \u003Cdiv class=\"card-body\">\n            \u003Cform action=\"\u002Fadmin\" method=\"GET\">\n                \u003Cdiv class=\"mb-2\">\n                    \u003Clabel for=\"free_word\" class=\"form-label\">キーワード\u003C\u002Flabel>\n                    \u003Cinput type=\"text\"\n                           class=\"form-control\"\n                           name=\"free_word\"\n                           id=\"free_word\"\n                           value=\"{{ $parameters['free_word'] ?? '' }}\">\n                \u003C\u002Fdiv>\n                \u003Cbutton type=\"submit\" class=\"btn btn-primary\">検索\u003C\u002Fbutton>\n                \u003Ca href=\"\u002Fadmin\" class=\"btn btn-secondary\">クリア\u003C\u002Fa>\n            \u003C\u002Fform>\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"mt-3\">\n        \u003Cp>検索結果: {{ $shops->total() }}件\u003C\u002Fp>\n        \u003Ctable class=\"table\">\n            \u003Cthead>\n                \u003Ctr>\n                    \u003Cth>ID\u003C\u002Fth>\n                    \u003Cth>名前\u003C\u002Fth>\n                    \u003Cth>年齢\u003C\u002Fth>\n                    \u003Cth>性別\u003C\u002Fth>\n                \u003C\u002Ftr>\n            \u003C\u002Fthead>\n            \u003Ctbody>\n                @foreach($shops as $shop)\n                    \u003Ctr>\n                        \u003Ctd>{{ $shop->id }}\u003C\u002Ftd>\n                        \u003Ctd>{{ $shop->name }}\u003C\u002Ftd>\n                        \u003Ctd>{{ $shop->age }}\u003C\u002Ftd>\n                        \u003Ctd>{{ $shop->gender_id === 1 ? '男性' : '女性' }}\u003C\u002Ftd>\n                    \u003C\u002Ftr>\n                @endforeach\n            \u003C\u002Ftbody>\n        \u003C\u002Ftable>\n        {{ $shops->links() }}\n    \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n",[18,952,953,958,963,968,973,978,983,988,993,998,1003,1008,1013,1018,1023,1028,1033,1038,1043,1048,1053,1058,1063,1068,1073,1078,1083,1088,1092,1097,1102,1107,1112,1117,1122,1127,1132,1137,1142,1147,1152,1157,1162,1167,1172,1177,1182,1187,1192,1198,1204,1210,1215,1221,1227],{"__ignoreMap":28},[32,954,955],{"class":34,"line":35},[32,956,957],{},"\u003C!DOCTYPE html>\n",[32,959,960],{"class":34,"line":41},[32,961,962],{},"\u003Chtml lang=\"{{ str_replace('_', '-', app()->getLocale()) }}\">\n",[32,964,965],{"class":34,"line":132},[32,966,967],{},"\u003Chead>\n",[32,969,970],{"class":34,"line":138},[32,971,972],{},"    \u003Cmeta charset=\"utf-8\">\n",[32,974,975],{"class":34,"line":143},[32,976,977],{},"    \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n",[32,979,980],{"class":34,"line":149},[32,981,982],{},"    \u003Ctitle>全文検索デモ\u003C\u002Ftitle>\n",[32,984,985],{"class":34,"line":155},[32,986,987],{},"    \u003Clink href=\"{{ asset('css\u002Fapp.css') }}\" rel=\"stylesheet\">\n",[32,989,990],{"class":34,"line":160},[32,991,992],{},"\u003C\u002Fhead>\n",[32,994,995],{"class":34,"line":166},[32,996,997],{},"\u003Cbody>\n",[32,999,1000],{"class":34,"line":172},[32,1001,1002],{},"\u003Cdiv class=\"container\">\n",[32,1004,1005],{"class":34,"line":178},[32,1006,1007],{},"    \u003Cdiv class=\"card\">\n",[32,1009,1010],{"class":34,"line":183},[32,1011,1012],{},"        \u003Cdiv class=\"card-header\">全文検索\u003C\u002Fdiv>\n",[32,1014,1015],{"class":34,"line":189},[32,1016,1017],{},"        \u003Cdiv class=\"card-body\">\n",[32,1019,1020],{"class":34,"line":194},[32,1021,1022],{},"            \u003Cform action=\"\u002Fadmin\" method=\"GET\">\n",[32,1024,1025],{"class":34,"line":200},[32,1026,1027],{},"                \u003Cdiv class=\"mb-2\">\n",[32,1029,1030],{"class":34,"line":289},[32,1031,1032],{},"                    \u003Clabel for=\"free_word\" class=\"form-label\">キーワード\u003C\u002Flabel>\n",[32,1034,1035],{"class":34,"line":295},[32,1036,1037],{},"                    \u003Cinput type=\"text\"\n",[32,1039,1040],{"class":34,"line":301},[32,1041,1042],{},"                           class=\"form-control\"\n",[32,1044,1045],{"class":34,"line":307},[32,1046,1047],{},"                           name=\"free_word\"\n",[32,1049,1050],{"class":34,"line":312},[32,1051,1052],{},"                           id=\"free_word\"\n",[32,1054,1055],{"class":34,"line":318},[32,1056,1057],{},"                           value=\"{{ $parameters['free_word'] ?? '' }}\">\n",[32,1059,1060],{"class":34,"line":324},[32,1061,1062],{},"                \u003C\u002Fdiv>\n",[32,1064,1065],{"class":34,"line":330},[32,1066,1067],{},"                \u003Cbutton type=\"submit\" class=\"btn btn-primary\">検索\u003C\u002Fbutton>\n",[32,1069,1070],{"class":34,"line":336},[32,1071,1072],{},"                \u003Ca href=\"\u002Fadmin\" class=\"btn btn-secondary\">クリア\u003C\u002Fa>\n",[32,1074,1075],{"class":34,"line":342},[32,1076,1077],{},"            \u003C\u002Fform>\n",[32,1079,1080],{"class":34,"line":348},[32,1081,1082],{},"        \u003C\u002Fdiv>\n",[32,1084,1085],{"class":34,"line":354},[32,1086,1087],{},"    \u003C\u002Fdiv>\n",[32,1089,1090],{"class":34,"line":360},[32,1091,129],{"emptyLinePlaceholder":128},[32,1093,1094],{"class":34,"line":366},[32,1095,1096],{},"    \u003Cdiv class=\"mt-3\">\n",[32,1098,1099],{"class":34,"line":372},[32,1100,1101],{},"        \u003Cp>検索結果: {{ $shops->total() }}件\u003C\u002Fp>\n",[32,1103,1104],{"class":34,"line":378},[32,1105,1106],{},"        \u003Ctable class=\"table\">\n",[32,1108,1109],{"class":34,"line":384},[32,1110,1111],{},"            \u003Cthead>\n",[32,1113,1114],{"class":34,"line":390},[32,1115,1116],{},"                \u003Ctr>\n",[32,1118,1119],{"class":34,"line":396},[32,1120,1121],{},"                    \u003Cth>ID\u003C\u002Fth>\n",[32,1123,1124],{"class":34,"line":402},[32,1125,1126],{},"                    \u003Cth>名前\u003C\u002Fth>\n",[32,1128,1129],{"class":34,"line":407},[32,1130,1131],{},"                    \u003Cth>年齢\u003C\u002Fth>\n",[32,1133,1134],{"class":34,"line":413},[32,1135,1136],{},"                    \u003Cth>性別\u003C\u002Fth>\n",[32,1138,1139],{"class":34,"line":418},[32,1140,1141],{},"                \u003C\u002Ftr>\n",[32,1143,1144],{"class":34,"line":423},[32,1145,1146],{},"            \u003C\u002Fthead>\n",[32,1148,1149],{"class":34,"line":429},[32,1150,1151],{},"            \u003Ctbody>\n",[32,1153,1154],{"class":34,"line":435},[32,1155,1156],{},"                @foreach($shops as $shop)\n",[32,1158,1159],{"class":34,"line":440},[32,1160,1161],{},"                    \u003Ctr>\n",[32,1163,1164],{"class":34,"line":446},[32,1165,1166],{},"                        \u003Ctd>{{ $shop->id }}\u003C\u002Ftd>\n",[32,1168,1169],{"class":34,"line":451},[32,1170,1171],{},"                        \u003Ctd>{{ $shop->name }}\u003C\u002Ftd>\n",[32,1173,1174],{"class":34,"line":457},[32,1175,1176],{},"                        \u003Ctd>{{ $shop->age }}\u003C\u002Ftd>\n",[32,1178,1179],{"class":34,"line":462},[32,1180,1181],{},"                        \u003Ctd>{{ $shop->gender_id === 1 ? '男性' : '女性' }}\u003C\u002Ftd>\n",[32,1183,1184],{"class":34,"line":468},[32,1185,1186],{},"                    \u003C\u002Ftr>\n",[32,1188,1189],{"class":34,"line":473},[32,1190,1191],{},"                @endforeach\n",[32,1193,1195],{"class":34,"line":1194},49,[32,1196,1197],{},"            \u003C\u002Ftbody>\n",[32,1199,1201],{"class":34,"line":1200},50,[32,1202,1203],{},"        \u003C\u002Ftable>\n",[32,1205,1207],{"class":34,"line":1206},51,[32,1208,1209],{},"        {{ $shops->links() }}\n",[32,1211,1213],{"class":34,"line":1212},52,[32,1214,1087],{},[32,1216,1218],{"class":34,"line":1217},53,[32,1219,1220],{},"\u003C\u002Fdiv>\n",[32,1222,1224],{"class":34,"line":1223},54,[32,1225,1226],{},"\u003C\u002Fbody>\n",[32,1228,1230],{"class":34,"line":1229},55,[32,1231,1232],{},"\u003C\u002Fhtml>\n",[10,1234,1235],{"id":1235},"セキュリティ上の注意点",[78,1237,1238],{"id":1238},"プリペアドステートメントの使用",[15,1240,1241,1244,1245,1248],{},[18,1242,1243],{},"whereRaw","を使う場合、必ずプレースホルダー（",[18,1246,1247],{},"?","）を使用してSQLインジェクション対策を行います。",[23,1250,1252],{"className":115,"code":1251,"language":94,"meta":28,"style":28},"\u002F\u002F ❌ 危険: SQLインジェクションのリスク\n$query->whereRaw(\"MATCH(free_word) AGAINST ('$freeWord' IN BOOLEAN MODE)\");\n\n\u002F\u002F ✅ 安全: プリペアドステートメント\n$query->whereRaw(\"MATCH(free_word) AGAINST (? IN BOOLEAN MODE)\", [$freeWord]);\n",[18,1253,1254,1259,1264,1268,1273],{"__ignoreMap":28},[32,1255,1256],{"class":34,"line":35},[32,1257,1258],{},"\u002F\u002F ❌ 危険: SQLインジェクションのリスク\n",[32,1260,1261],{"class":34,"line":41},[32,1262,1263],{},"$query->whereRaw(\"MATCH(free_word) AGAINST ('$freeWord' IN BOOLEAN MODE)\");\n",[32,1265,1266],{"class":34,"line":132},[32,1267,129],{"emptyLinePlaceholder":128},[32,1269,1270],{"class":34,"line":138},[32,1271,1272],{},"\u002F\u002F ✅ 安全: プリペアドステートメント\n",[32,1274,1275],{"class":34,"line":143},[32,1276,1277],{},"$query->whereRaw(\"MATCH(free_word) AGAINST (? IN BOOLEAN MODE)\", [$freeWord]);\n",[78,1279,1280],{"id":1280},"入力値のバリデーション",[23,1282,1284],{"className":115,"code":1283,"language":94,"meta":28,"style":28},"$validated = $request->validate([\n    'free_word' => 'nullable|string|max:255',\n]);\n",[18,1285,1286,1291,1296],{"__ignoreMap":28},[32,1287,1288],{"class":34,"line":35},[32,1289,1290],{},"$validated = $request->validate([\n",[32,1292,1293],{"class":34,"line":41},[32,1294,1295],{},"    'free_word' => 'nullable|string|max:255',\n",[32,1297,1298],{"class":34,"line":132},[32,1299,1300],{},"]);\n",[10,1302,1304],{"id":1303},"like検索との比較","LIKE検索との比較",[78,1306,1308],{"id":1307},"like検索","LIKE検索",[23,1310,1312],{"className":115,"code":1311,"language":94,"meta":28,"style":28},"\u002F\u002F インデックスが使えない\n$query->where('name', 'LIKE', \"%{$keyword}%\");\n",[18,1313,1314,1319],{"__ignoreMap":28},[32,1315,1316],{"class":34,"line":35},[32,1317,1318],{},"\u002F\u002F インデックスが使えない\n",[32,1320,1321],{"class":34,"line":41},[32,1322,1323],{},"$query->where('name', 'LIKE', \"%{$keyword}%\");\n",[494,1325,1326,1332],{},[497,1327,1328,1331],{},[56,1329,1330],{},"メリット",": シンプルな実装",[497,1333,1334,1337],{},[56,1335,1336],{},"デメリット",": データ量が増えると遅い、インデックスが使えない",[78,1339,1340],{"id":1340},"全文検索",[23,1342,1344],{"className":115,"code":1343,"language":94,"meta":28,"style":28},"\u002F\u002F 全文検索インデックスを使用\n$query->whereRaw(\"MATCH(free_word) AGAINST (? IN BOOLEAN MODE)\", [$keyword]);\n",[18,1345,1346,1351],{"__ignoreMap":28},[32,1347,1348],{"class":34,"line":35},[32,1349,1350],{},"\u002F\u002F 全文検索インデックスを使用\n",[32,1352,1353],{"class":34,"line":41},[32,1354,1355],{},"$query->whereRaw(\"MATCH(free_word) AGAINST (? IN BOOLEAN MODE)\", [$keyword]);\n",[494,1357,1358,1363],{},[497,1359,1360,1362],{},[56,1361,1330],{},": 高速、大量データでも性能が安定",[497,1364,1365,1367],{},[56,1366,1336],{},": セットアップが必要、ストレージ使用量が増える",[10,1369,1370],{"id":1370},"まとめ",[15,1372,1373],{},"MySQLの全文検索を使うことで",[494,1375,1376,1382,1388],{},[497,1377,1378,1381],{},[56,1379,1380],{},"高速な検索",": インデックスを活用した効率的な検索",[497,1383,1384,1387],{},[56,1385,1386],{},"日本語対応",": N-gramパーサーで日本語の部分一致検索が可能",[497,1389,1390,1393],{},[56,1391,1392],{},"柔軟な検索",": BOOLEANモードでAND\u002FOR\u002FNOT検索に対応",[15,1395,1396],{},"大量のテキストデータを検索する必要がある場合、全文検索の導入を検討する価値があります。",[78,1398,1399],{"id":1399},"参考リンク",[494,1401,1402,1411,1417],{},[497,1403,1404],{},[1405,1406,1410],"a",{"href":1407,"rel":1408},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fja\u002Ffulltext-search.html",[1409],"nofollow","MySQL 全文検索",[497,1412,1413],{},[1405,1414,58],{"href":1415,"rel":1416},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fja\u002Ffulltext-search-ngram.html",[1409],[497,1418,1419],{},[1405,1420,1423],{"href":1421,"rel":1422},"https:\u002F\u002Flaravel.com\u002Fdocs\u002Fqueries",[1409],"Laravel クエリビルダ",[1425,1426,1427],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":28,"searchDepth":41,"depth":41,"links":1429},[1430,1431,1432,1442,1446,1450],{"id":12,"depth":41,"text":13},{"id":50,"depth":41,"text":51},{"id":76,"depth":41,"text":76,"children":1433},[1434,1435,1436,1437,1438,1439,1440,1441],{"id":80,"depth":132,"text":81},{"id":111,"depth":132,"text":112},{"id":206,"depth":132,"text":207},{"id":478,"depth":132,"text":478},{"id":580,"depth":132,"text":581},{"id":706,"depth":132,"text":707},{"id":863,"depth":132,"text":863},{"id":946,"depth":132,"text":947},{"id":1235,"depth":41,"text":1235,"children":1443},[1444,1445],{"id":1238,"depth":132,"text":1238},{"id":1280,"depth":132,"text":1280},{"id":1303,"depth":41,"text":1304,"children":1447},[1448,1449],{"id":1307,"depth":132,"text":1308},{"id":1340,"depth":132,"text":1340},{"id":1370,"depth":41,"text":1370,"children":1451},[1452],{"id":1399,"depth":132,"text":1399},"2022-01-02","MySQLのN-gram全文検索インデックスを使って、Laravelアプリケーションに高速な全文検索機能を実装する方法を解説します。","md",{"tags":1457},[1458,1459,1460],"laravel","mysql","fulltext-search","\u002Fblog\u002Flaravel-mysql-fulltext-search",{"title":5,"description":1454},"blog\u002Flaravel-mysql-fulltext-search","YA6hFRcXoo23j2st071q-vuQtFTveg1AndRjg63Leu4",1773664054161]