[{"data":1,"prerenderedAt":1750},["ShallowReactive",2],{"layout-sidebar-\u002Fblog\u002Ftypesense-schema-migrations-laravel":3,"blog-typesense-schema-migrations-laravel":41},{"type":4,"author":5,"date":6,"status":7,"toc":8},"blog-detail","Fabian Kirchhoff","2026-05-10","published",[9,13,16,20,23,26,29,32,35,38],{"id":10,"text":11,"depth":12},"how-laravel-scout-indexes-data","How Laravel Scout Indexes Data",2,{"id":14,"text":15,"depth":12},"the-problem-schema-changes","The Problem: Schema Changes",{"id":17,"text":18,"depth":19},"option-a-flush-and-recreate","Option A: Flush and Recreate",3,{"id":21,"text":22,"depth":19},"option-b-patch-the-schema","Option B: Patch the Schema",{"id":24,"text":25,"depth":12},"typesenseschemaservice","TypesenseSchemaService",{"id":27,"text":28,"depth":12},"the-migration","The Migration",{"id":30,"text":31,"depth":12},"what-happens-after-the-migration","What Happens After the Migration",{"id":33,"text":34,"depth":12},"comparison","Comparison",{"id":36,"text":37,"depth":12},"when-to-use-each-approach","When to Use Each Approach",{"id":39,"text":40,"depth":12},"resources","Resources",{"id":42,"title":43,"author":5,"body":44,"date":6,"description":1736,"extension":1737,"featured":1738,"meta":1739,"navigation":120,"order":1740,"path":1741,"seo":1742,"specs":1743,"status":7,"stem":1747,"tag":1748,"__hash__":1749},"blog\u002Fblog\u002Ftypesense-schema-migrations-laravel.md","Typesense Schema Migrations in Laravel",{"type":45,"value":46,"toc":1723},"minimark",[47,51,55,58,61,69,296,299,332,342,344,351,354,385,388,406,412,414,425,427,430,930,933,954,956,959,1192,1199,1206,1212,1381,1392,1445,1448,1502,1504,1516,1526,1536,1546,1573,1582,1584,1651,1653,1658,1669,1675,1692,1695,1697,1719],[48,49,43],"h1",{"id":50},"typesense-schema-migrations-in-laravel",[52,53,54],"p",{},"Adding a field to a Typesense collection usually means: flush the collection, recreate it with the new schema, re-import every document. For a collection with hundreds of thousands of documents, that means minutes of downtime where search doesn't work.",[52,56,57],{},"I needed a better way. My order search index has hundreds of thousands of documents. Flushing and re-indexing takes time, and during that window, users can't search. So I built a migration-based approach that patches the live Typesense collection in place — no downtime, no data loss.",[59,60,11],"h2",{"id":10},[52,62,63,64,68],{},"Scout adds a ",[65,66,67],"code",{},"Searchable"," trait to Eloquent models. The trait hooks into model lifecycle events — when a model is created, updated, or deleted, Scout automatically syncs it to Typesense.",[70,71,76],"pre",{"className":72,"code":73,"language":74,"meta":75,"style":75},"language-php shiki shiki-themes github-dark github-dark","class Order extends Model\n{\n    use Searchable;\n\n    public function toSearchableArray(): array\n    {\n        return [\n            'id' => (string) $this->id,\n            'number' => $this->number,\n            'customer_name' => $this->customer?->name,\n            'status' => $this->status->value,\n            'total' => $this->total,\n            'created_at' => $this->created_at?->timestamp,\n        ];\n    }\n}\n","php","",[65,77,78,97,103,115,122,143,149,158,186,202,223,243,258,278,284,290],{"__ignoreMap":75},[79,80,83,87,91,94],"span",{"class":81,"line":82},"line",1,[79,84,86],{"class":85},"sOPea","class",[79,88,90],{"class":89},"sFR8T"," Order",[79,92,93],{"class":85}," extends",[79,95,96],{"class":89}," Model\n",[79,98,99],{"class":81,"line":12},[79,100,102],{"class":101},"suv1-","{\n",[79,104,105,108,112],{"class":81,"line":19},[79,106,107],{"class":85},"    use",[79,109,111],{"class":110},"s8ozJ"," Searchable",[79,113,114],{"class":101},";\n",[79,116,118],{"class":81,"line":117},4,[79,119,121],{"emptyLinePlaceholder":120},true,"\n",[79,123,125,128,131,134,137,140],{"class":81,"line":124},5,[79,126,127],{"class":85},"    public",[79,129,130],{"class":85}," function",[79,132,133],{"class":89}," toSearchableArray",[79,135,136],{"class":101},"()",[79,138,139],{"class":85},":",[79,141,142],{"class":85}," array\n",[79,144,146],{"class":81,"line":145},6,[79,147,148],{"class":101},"    {\n",[79,150,152,155],{"class":81,"line":151},7,[79,153,154],{"class":85},"        return",[79,156,157],{"class":101}," [\n",[79,159,161,165,168,171,174,177,180,183],{"class":81,"line":160},8,[79,162,164],{"class":163},"s4wv1","            'id'",[79,166,167],{"class":85}," =>",[79,169,170],{"class":101}," (",[79,172,173],{"class":85},"string",[79,175,176],{"class":101},") ",[79,178,179],{"class":110},"$this",[79,181,182],{"class":85},"->",[79,184,185],{"class":101},"id,\n",[79,187,189,192,194,197,199],{"class":81,"line":188},9,[79,190,191],{"class":163},"            'number'",[79,193,167],{"class":85},[79,195,196],{"class":110}," $this",[79,198,182],{"class":85},[79,200,201],{"class":101},"number,\n",[79,203,205,208,210,212,214,217,220],{"class":81,"line":204},10,[79,206,207],{"class":163},"            'customer_name'",[79,209,167],{"class":85},[79,211,196],{"class":110},[79,213,182],{"class":85},[79,215,216],{"class":101},"customer",[79,218,219],{"class":85},"?->",[79,221,222],{"class":101},"name,\n",[79,224,226,229,231,233,235,238,240],{"class":81,"line":225},11,[79,227,228],{"class":163},"            'status'",[79,230,167],{"class":85},[79,232,196],{"class":110},[79,234,182],{"class":85},[79,236,237],{"class":101},"status",[79,239,182],{"class":85},[79,241,242],{"class":101},"value,\n",[79,244,246,249,251,253,255],{"class":81,"line":245},12,[79,247,248],{"class":163},"            'total'",[79,250,167],{"class":85},[79,252,196],{"class":110},[79,254,182],{"class":85},[79,256,257],{"class":101},"total,\n",[79,259,261,264,266,268,270,273,275],{"class":81,"line":260},13,[79,262,263],{"class":163},"            'created_at'",[79,265,167],{"class":85},[79,267,196],{"class":110},[79,269,182],{"class":85},[79,271,272],{"class":101},"created_at",[79,274,219],{"class":85},[79,276,277],{"class":101},"timestamp,\n",[79,279,281],{"class":81,"line":280},14,[79,282,283],{"class":101},"        ];\n",[79,285,287],{"class":81,"line":286},15,[79,288,289],{"class":101},"    }\n",[79,291,293],{"class":81,"line":292},16,[79,294,295],{"class":101},"}\n",[52,297,298],{},"The data flow:",[300,301,302,314,320,326],"ol",{},[303,304,305,309,310,313],"li",{},[306,307,308],"strong",{},"Model saved"," → Scout's ",[65,311,312],{},"ModelObserver"," fires",[303,315,316,319],{},[65,317,318],{},"shouldBeSearchable()"," → decides if the model belongs in the index",[303,321,322,325],{},[65,323,324],{},"toSearchableArray()"," → serializes the model into a flat document",[303,327,328,331],{},[65,329,330],{},"TypesenseEngine::update()"," → bulk upserts documents via Typesense's JSONL import API",[52,333,334,335,338,339,341],{},"For bulk imports, ",[65,336,337],{},"php artisan scout:import \"App\\Models\\Order\""," processes records in chunks of 500, calling ",[65,340,324],{}," on each and upserting them.",[59,343,15],{"id":14},[52,345,346,347,350],{},"Typesense collections have a strict schema. Every field needs a name, type, and configuration before documents can include it. When I wanted to add a ",[65,348,349],{},"tags"," field to orders, I had two options:",[352,353,18],"h3",{"id":17},[70,355,359],{"className":356,"code":357,"language":358,"meta":75,"style":75},"language-bash shiki shiki-themes github-dark github-dark","php artisan scout:flush \"App\\Models\\Order\"\nphp artisan scout:import \"App\\Models\\Order\"\n","bash",[65,360,361,374],{"__ignoreMap":75},[79,362,363,365,368,371],{"class":81,"line":82},[79,364,74],{"class":89},[79,366,367],{"class":163}," artisan",[79,369,370],{"class":163}," scout:flush",[79,372,373],{"class":163}," \"App\\Models\\Order\"\n",[79,375,376,378,380,383],{"class":81,"line":12},[79,377,74],{"class":89},[79,379,367],{"class":163},[79,381,382],{"class":163}," scout:import",[79,384,373],{"class":163},[52,386,387],{},"This works, but:",[389,390,391,397,400,403],"ul",{},[303,392,393,396],{},[65,394,395],{},"scout:flush"," deletes the entire Typesense collection — not just documents, the collection itself",[303,398,399],{},"During re-import, search returns no results",[303,401,402],{},"Re-indexing 500k orders takes several minutes",[303,404,405],{},"If the import fails halfway, you're left with a partial index",[52,407,408,411],{},[65,409,410],{},"scout:import"," alone (without flush) upserts documents into the existing collection — it doesn't require a flush. The flush here is only needed because the collection schema itself needs to change.",[352,413,22],{"id":21},[52,415,416,417,424],{},"Typesense's ",[418,419,423],"a",{"href":420,"rel":421},"https:\u002F\u002Ftypesense.org\u002Fdocs\u002F27.1\u002Fapi\u002Fcollections.html#update-or-alter-a-collection",[422],"nofollow","Collection API"," supports PATCH requests to add or remove fields from an existing collection without touching the documents already in it.",[59,426,25],{"id":24},[52,428,429],{},"I built a service that wraps the Typesense PHP client with idempotent field-level operations:",[70,431,433],{"className":72,"code":432,"language":74,"meta":75,"style":75},"class TypesenseSchemaService\n{\n    public static function isEnabled(): bool\n    {\n        return config('scout.driver') === 'typesense';\n    }\n\n    public function addField(string $collection, array $fieldDefinition): void\n    {\n        if ($this->fieldExists($collection, $fieldDefinition['name'])) {\n            return;\n        }\n\n        $this->client->collections[$this->prefixed($collection)]->update([\n            'fields' => [$fieldDefinition],\n        ]);\n    }\n\n    public function dropField(string $collection, string $fieldName): void\n    {\n        if (!$this->fieldExists($collection, $fieldName)) {\n            return;\n        }\n\n        $this->client->collections[$this->prefixed($collection)]->update([\n            'fields' => [['name' => $fieldName, 'drop' => true]],\n        ]);\n    }\n\n    private function fieldExists(string $collection, string $fieldName): bool\n    {\n        $schema = $this->client->collections[$this->prefixed($collection)]->retrieve();\n        return in_array($fieldName, array_column($schema['fields'], 'name'), true);\n    }\n\n    private function prefixed(string $collection): string\n    {\n        return config('scout.prefix') . $collection;\n    }\n}\n",[65,434,435,442,446,465,469,492,496,500,527,531,554,561,566,570,603,613,618,623,628,653,658,677,684,689,694,721,749,754,759,764,789,794,829,863,868,873,895,900,920,925],{"__ignoreMap":75},[79,436,437,439],{"class":81,"line":82},[79,438,86],{"class":85},[79,440,441],{"class":89}," TypesenseSchemaService\n",[79,443,444],{"class":81,"line":12},[79,445,102],{"class":101},[79,447,448,450,453,455,458,460,462],{"class":81,"line":19},[79,449,127],{"class":85},[79,451,452],{"class":85}," static",[79,454,130],{"class":85},[79,456,457],{"class":89}," isEnabled",[79,459,136],{"class":101},[79,461,139],{"class":85},[79,463,464],{"class":85}," bool\n",[79,466,467],{"class":81,"line":117},[79,468,148],{"class":101},[79,470,471,473,476,479,482,484,487,490],{"class":81,"line":124},[79,472,154],{"class":85},[79,474,475],{"class":89}," config",[79,477,478],{"class":101},"(",[79,480,481],{"class":163},"'scout.driver'",[79,483,176],{"class":101},[79,485,486],{"class":85},"===",[79,488,489],{"class":163}," 'typesense'",[79,491,114],{"class":101},[79,493,494],{"class":81,"line":145},[79,495,289],{"class":101},[79,497,498],{"class":81,"line":151},[79,499,121],{"emptyLinePlaceholder":120},[79,501,502,504,506,509,511,513,516,519,522,524],{"class":81,"line":160},[79,503,127],{"class":85},[79,505,130],{"class":85},[79,507,508],{"class":89}," addField",[79,510,478],{"class":101},[79,512,173],{"class":85},[79,514,515],{"class":101}," $collection, ",[79,517,518],{"class":85},"array",[79,520,521],{"class":101}," $fieldDefinition)",[79,523,139],{"class":85},[79,525,526],{"class":85}," void\n",[79,528,529],{"class":81,"line":188},[79,530,148],{"class":101},[79,532,533,536,538,540,542,545,548,551],{"class":81,"line":204},[79,534,535],{"class":85},"        if",[79,537,170],{"class":101},[79,539,179],{"class":110},[79,541,182],{"class":85},[79,543,544],{"class":89},"fieldExists",[79,546,547],{"class":101},"($collection, $fieldDefinition[",[79,549,550],{"class":163},"'name'",[79,552,553],{"class":101},"])) {\n",[79,555,556,559],{"class":81,"line":225},[79,557,558],{"class":85},"            return",[79,560,114],{"class":101},[79,562,563],{"class":81,"line":245},[79,564,565],{"class":101},"        }\n",[79,567,568],{"class":81,"line":260},[79,569,121],{"emptyLinePlaceholder":120},[79,571,572,575,577,580,582,585,587,589,592,595,597,600],{"class":81,"line":280},[79,573,574],{"class":110},"        $this",[79,576,182],{"class":85},[79,578,579],{"class":101},"client",[79,581,182],{"class":85},[79,583,584],{"class":101},"collections[",[79,586,179],{"class":110},[79,588,182],{"class":85},[79,590,591],{"class":89},"prefixed",[79,593,594],{"class":101},"($collection)]",[79,596,182],{"class":85},[79,598,599],{"class":89},"update",[79,601,602],{"class":101},"([\n",[79,604,605,608,610],{"class":81,"line":286},[79,606,607],{"class":163},"            'fields'",[79,609,167],{"class":85},[79,611,612],{"class":101}," [$fieldDefinition],\n",[79,614,615],{"class":81,"line":292},[79,616,617],{"class":101},"        ]);\n",[79,619,621],{"class":81,"line":620},17,[79,622,289],{"class":101},[79,624,626],{"class":81,"line":625},18,[79,627,121],{"emptyLinePlaceholder":120},[79,629,631,633,635,638,640,642,644,646,649,651],{"class":81,"line":630},19,[79,632,127],{"class":85},[79,634,130],{"class":85},[79,636,637],{"class":89}," dropField",[79,639,478],{"class":101},[79,641,173],{"class":85},[79,643,515],{"class":101},[79,645,173],{"class":85},[79,647,648],{"class":101}," $fieldName)",[79,650,139],{"class":85},[79,652,526],{"class":85},[79,654,656],{"class":81,"line":655},20,[79,657,148],{"class":101},[79,659,661,663,665,668,670,672,674],{"class":81,"line":660},21,[79,662,535],{"class":85},[79,664,170],{"class":101},[79,666,667],{"class":85},"!",[79,669,179],{"class":110},[79,671,182],{"class":85},[79,673,544],{"class":89},[79,675,676],{"class":101},"($collection, $fieldName)) {\n",[79,678,680,682],{"class":81,"line":679},22,[79,681,558],{"class":85},[79,683,114],{"class":101},[79,685,687],{"class":81,"line":686},23,[79,688,565],{"class":101},[79,690,692],{"class":81,"line":691},24,[79,693,121],{"emptyLinePlaceholder":120},[79,695,697,699,701,703,705,707,709,711,713,715,717,719],{"class":81,"line":696},25,[79,698,574],{"class":110},[79,700,182],{"class":85},[79,702,579],{"class":101},[79,704,182],{"class":85},[79,706,584],{"class":101},[79,708,179],{"class":110},[79,710,182],{"class":85},[79,712,591],{"class":89},[79,714,594],{"class":101},[79,716,182],{"class":85},[79,718,599],{"class":89},[79,720,602],{"class":101},[79,722,724,726,728,731,733,735,738,741,743,746],{"class":81,"line":723},26,[79,725,607],{"class":163},[79,727,167],{"class":85},[79,729,730],{"class":101}," [[",[79,732,550],{"class":163},[79,734,167],{"class":85},[79,736,737],{"class":101}," $fieldName, ",[79,739,740],{"class":163},"'drop'",[79,742,167],{"class":85},[79,744,745],{"class":110}," true",[79,747,748],{"class":101},"]],\n",[79,750,752],{"class":81,"line":751},27,[79,753,617],{"class":101},[79,755,757],{"class":81,"line":756},28,[79,758,289],{"class":101},[79,760,762],{"class":81,"line":761},29,[79,763,121],{"emptyLinePlaceholder":120},[79,765,767,770,772,775,777,779,781,783,785,787],{"class":81,"line":766},30,[79,768,769],{"class":85},"    private",[79,771,130],{"class":85},[79,773,774],{"class":89}," fieldExists",[79,776,478],{"class":101},[79,778,173],{"class":85},[79,780,515],{"class":101},[79,782,173],{"class":85},[79,784,648],{"class":101},[79,786,139],{"class":85},[79,788,464],{"class":85},[79,790,792],{"class":81,"line":791},31,[79,793,148],{"class":101},[79,795,797,800,803,805,807,809,811,813,815,817,819,821,823,826],{"class":81,"line":796},32,[79,798,799],{"class":101},"        $schema ",[79,801,802],{"class":85},"=",[79,804,196],{"class":110},[79,806,182],{"class":85},[79,808,579],{"class":101},[79,810,182],{"class":85},[79,812,584],{"class":101},[79,814,179],{"class":110},[79,816,182],{"class":85},[79,818,591],{"class":89},[79,820,594],{"class":101},[79,822,182],{"class":85},[79,824,825],{"class":89},"retrieve",[79,827,828],{"class":101},"();\n",[79,830,832,834,837,840,843,846,849,852,854,857,860],{"class":81,"line":831},33,[79,833,154],{"class":85},[79,835,836],{"class":110}," in_array",[79,838,839],{"class":101},"($fieldName, ",[79,841,842],{"class":110},"array_column",[79,844,845],{"class":101},"($schema[",[79,847,848],{"class":163},"'fields'",[79,850,851],{"class":101},"], ",[79,853,550],{"class":163},[79,855,856],{"class":101},"), ",[79,858,859],{"class":110},"true",[79,861,862],{"class":101},");\n",[79,864,866],{"class":81,"line":865},34,[79,867,289],{"class":101},[79,869,871],{"class":81,"line":870},35,[79,872,121],{"emptyLinePlaceholder":120},[79,874,876,878,880,883,885,887,890,892],{"class":81,"line":875},36,[79,877,769],{"class":85},[79,879,130],{"class":85},[79,881,882],{"class":89}," prefixed",[79,884,478],{"class":101},[79,886,173],{"class":85},[79,888,889],{"class":101}," $collection)",[79,891,139],{"class":85},[79,893,894],{"class":85}," string\n",[79,896,898],{"class":81,"line":897},37,[79,899,148],{"class":101},[79,901,903,905,907,909,912,914,917],{"class":81,"line":902},38,[79,904,154],{"class":85},[79,906,475],{"class":89},[79,908,478],{"class":101},[79,910,911],{"class":163},"'scout.prefix'",[79,913,176],{"class":101},[79,915,916],{"class":85},".",[79,918,919],{"class":101}," $collection;\n",[79,921,923],{"class":81,"line":922},39,[79,924,289],{"class":101},[79,926,928],{"class":81,"line":927},40,[79,929,295],{"class":101},[52,931,932],{},"Two things matter:",[300,934,935,945],{},[303,936,937,940,941,944],{},[306,938,939],{},"Idempotent"," — ",[65,942,943],{},"addField"," checks if the field exists before adding. Running the same migration twice doesn't fail.",[303,946,947,940,950,953],{},[306,948,949],{},"Reversible",[65,951,952],{},"dropField"," removes a field cleanly. Standard migration rollback.",[59,955,28],{"id":27},[52,957,958],{},"The key insight: treat Typesense schema changes like database schema changes. Use Laravel migrations.",[70,960,962],{"className":72,"code":961,"language":74,"meta":75,"style":75},"return new class extends Migration\n{\n    public function up(): void\n    {\n        if (!TypesenseSchemaService::isEnabled()) {\n            return;\n        }\n\n        TypesenseSchemaService::make()->addField('orders', [\n            'name' => 'tags',\n            'type' => 'string[]',\n            'optional' => true,\n        ]);\n    }\n\n    public function down(): void\n    {\n        if (!TypesenseSchemaService::isEnabled()) {\n            return;\n        }\n\n        TypesenseSchemaService::make()->dropField('orders', 'tags');\n    }\n};\n",[65,963,964,980,984,999,1003,1022,1028,1032,1036,1060,1073,1085,1096,1100,1104,1108,1123,1127,1143,1149,1153,1157,1183,1187],{"__ignoreMap":75},[79,965,966,969,972,975,977],{"class":81,"line":82},[79,967,968],{"class":85},"return",[79,970,971],{"class":85}," new",[79,973,974],{"class":85}," class",[79,976,93],{"class":85},[79,978,979],{"class":89}," Migration\n",[79,981,982],{"class":81,"line":12},[79,983,102],{"class":101},[79,985,986,988,990,993,995,997],{"class":81,"line":19},[79,987,127],{"class":85},[79,989,130],{"class":85},[79,991,992],{"class":89}," up",[79,994,136],{"class":101},[79,996,139],{"class":85},[79,998,526],{"class":85},[79,1000,1001],{"class":81,"line":117},[79,1002,148],{"class":101},[79,1004,1005,1007,1009,1011,1013,1016,1019],{"class":81,"line":124},[79,1006,535],{"class":85},[79,1008,170],{"class":101},[79,1010,667],{"class":85},[79,1012,25],{"class":110},[79,1014,1015],{"class":85},"::",[79,1017,1018],{"class":89},"isEnabled",[79,1020,1021],{"class":101},"()) {\n",[79,1023,1024,1026],{"class":81,"line":145},[79,1025,558],{"class":85},[79,1027,114],{"class":101},[79,1029,1030],{"class":81,"line":151},[79,1031,565],{"class":101},[79,1033,1034],{"class":81,"line":160},[79,1035,121],{"emptyLinePlaceholder":120},[79,1037,1038,1041,1043,1046,1048,1050,1052,1054,1057],{"class":81,"line":188},[79,1039,1040],{"class":110},"        TypesenseSchemaService",[79,1042,1015],{"class":85},[79,1044,1045],{"class":89},"make",[79,1047,136],{"class":101},[79,1049,182],{"class":85},[79,1051,943],{"class":89},[79,1053,478],{"class":101},[79,1055,1056],{"class":163},"'orders'",[79,1058,1059],{"class":101},", [\n",[79,1061,1062,1065,1067,1070],{"class":81,"line":204},[79,1063,1064],{"class":163},"            'name'",[79,1066,167],{"class":85},[79,1068,1069],{"class":163}," 'tags'",[79,1071,1072],{"class":101},",\n",[79,1074,1075,1078,1080,1083],{"class":81,"line":225},[79,1076,1077],{"class":163},"            'type'",[79,1079,167],{"class":85},[79,1081,1082],{"class":163}," 'string[]'",[79,1084,1072],{"class":101},[79,1086,1087,1090,1092,1094],{"class":81,"line":245},[79,1088,1089],{"class":163},"            'optional'",[79,1091,167],{"class":85},[79,1093,745],{"class":110},[79,1095,1072],{"class":101},[79,1097,1098],{"class":81,"line":260},[79,1099,617],{"class":101},[79,1101,1102],{"class":81,"line":280},[79,1103,289],{"class":101},[79,1105,1106],{"class":81,"line":286},[79,1107,121],{"emptyLinePlaceholder":120},[79,1109,1110,1112,1114,1117,1119,1121],{"class":81,"line":292},[79,1111,127],{"class":85},[79,1113,130],{"class":85},[79,1115,1116],{"class":89}," down",[79,1118,136],{"class":101},[79,1120,139],{"class":85},[79,1122,526],{"class":85},[79,1124,1125],{"class":81,"line":620},[79,1126,148],{"class":101},[79,1128,1129,1131,1133,1135,1137,1139,1141],{"class":81,"line":625},[79,1130,535],{"class":85},[79,1132,170],{"class":101},[79,1134,667],{"class":85},[79,1136,25],{"class":110},[79,1138,1015],{"class":85},[79,1140,1018],{"class":89},[79,1142,1021],{"class":101},[79,1144,1145,1147],{"class":81,"line":630},[79,1146,558],{"class":85},[79,1148,114],{"class":101},[79,1150,1151],{"class":81,"line":655},[79,1152,565],{"class":101},[79,1154,1155],{"class":81,"line":660},[79,1156,121],{"emptyLinePlaceholder":120},[79,1158,1159,1161,1163,1165,1167,1169,1171,1173,1175,1178,1181],{"class":81,"line":679},[79,1160,1040],{"class":110},[79,1162,1015],{"class":85},[79,1164,1045],{"class":89},[79,1166,136],{"class":101},[79,1168,182],{"class":85},[79,1170,952],{"class":89},[79,1172,478],{"class":101},[79,1174,1056],{"class":163},[79,1176,1177],{"class":101},", ",[79,1179,1180],{"class":163},"'tags'",[79,1182,862],{"class":101},[79,1184,1185],{"class":81,"line":686},[79,1186,289],{"class":101},[79,1188,1189],{"class":81,"line":691},[79,1190,1191],{"class":101},"};\n",[52,1193,1194,1195,1198],{},"The ",[65,1196,1197],{},"isEnabled()"," guard is important — in test environments or local setups without Typesense, the migration is a no-op.",[52,1200,1201,1202,1205],{},"This deploys with ",[65,1203,1204],{},"php artisan migrate",", right alongside your database migrations. No separate deployment step, no manual commands.",[52,1207,1208,1209,1211],{},"The model's ",[65,1210,324],{}," needs to include the new field:",[70,1213,1215],{"className":72,"code":1214,"language":74,"meta":75,"style":75},"public function toSearchableArray(): array\n{\n    return [\n        'id' => (string) $this->id,\n        'number' => $this->number,\n        'customer_name' => $this->customer?->name,\n        'status' => $this->status->value,\n        'total' => $this->total,\n        'created_at' => $this->created_at?->timestamp,\n        'tags' => $this->tags->pluck('name')->all(),\n    ];\n}\n",[65,1216,1217,1232,1236,1243,1262,1275,1292,1309,1322,1339,1372,1377],{"__ignoreMap":75},[79,1218,1219,1222,1224,1226,1228,1230],{"class":81,"line":82},[79,1220,1221],{"class":85},"public",[79,1223,130],{"class":85},[79,1225,133],{"class":89},[79,1227,136],{"class":101},[79,1229,139],{"class":85},[79,1231,142],{"class":85},[79,1233,1234],{"class":81,"line":12},[79,1235,102],{"class":101},[79,1237,1238,1241],{"class":81,"line":19},[79,1239,1240],{"class":85},"    return",[79,1242,157],{"class":101},[79,1244,1245,1248,1250,1252,1254,1256,1258,1260],{"class":81,"line":117},[79,1246,1247],{"class":163},"        'id'",[79,1249,167],{"class":85},[79,1251,170],{"class":101},[79,1253,173],{"class":85},[79,1255,176],{"class":101},[79,1257,179],{"class":110},[79,1259,182],{"class":85},[79,1261,185],{"class":101},[79,1263,1264,1267,1269,1271,1273],{"class":81,"line":124},[79,1265,1266],{"class":163},"        'number'",[79,1268,167],{"class":85},[79,1270,196],{"class":110},[79,1272,182],{"class":85},[79,1274,201],{"class":101},[79,1276,1277,1280,1282,1284,1286,1288,1290],{"class":81,"line":145},[79,1278,1279],{"class":163},"        'customer_name'",[79,1281,167],{"class":85},[79,1283,196],{"class":110},[79,1285,182],{"class":85},[79,1287,216],{"class":101},[79,1289,219],{"class":85},[79,1291,222],{"class":101},[79,1293,1294,1297,1299,1301,1303,1305,1307],{"class":81,"line":151},[79,1295,1296],{"class":163},"        'status'",[79,1298,167],{"class":85},[79,1300,196],{"class":110},[79,1302,182],{"class":85},[79,1304,237],{"class":101},[79,1306,182],{"class":85},[79,1308,242],{"class":101},[79,1310,1311,1314,1316,1318,1320],{"class":81,"line":160},[79,1312,1313],{"class":163},"        'total'",[79,1315,167],{"class":85},[79,1317,196],{"class":110},[79,1319,182],{"class":85},[79,1321,257],{"class":101},[79,1323,1324,1327,1329,1331,1333,1335,1337],{"class":81,"line":188},[79,1325,1326],{"class":163},"        'created_at'",[79,1328,167],{"class":85},[79,1330,196],{"class":110},[79,1332,182],{"class":85},[79,1334,272],{"class":101},[79,1336,219],{"class":85},[79,1338,277],{"class":101},[79,1340,1341,1344,1346,1348,1350,1352,1354,1357,1359,1361,1364,1366,1369],{"class":81,"line":204},[79,1342,1343],{"class":163},"        'tags'",[79,1345,167],{"class":85},[79,1347,196],{"class":110},[79,1349,182],{"class":85},[79,1351,349],{"class":101},[79,1353,182],{"class":85},[79,1355,1356],{"class":89},"pluck",[79,1358,478],{"class":101},[79,1360,550],{"class":163},[79,1362,1363],{"class":101},")",[79,1365,182],{"class":85},[79,1367,1368],{"class":89},"all",[79,1370,1371],{"class":101},"(),\n",[79,1373,1374],{"class":81,"line":225},[79,1375,1376],{"class":101},"    ];\n",[79,1378,1379],{"class":81,"line":245},[79,1380,295],{"class":101},[52,1382,1383,1384,1387,1388,1391],{},"Accessing ",[65,1385,1386],{},"$this->tags"," triggers a query per model. During bulk indexing, this causes N+1 queries. Use ",[65,1389,1390],{},"makeAllSearchableUsing()"," to eager-load relationships before indexing:",[70,1393,1395],{"className":72,"code":1394,"language":74,"meta":75,"style":75},"public function makeAllSearchableUsing(Builder $query): Builder\n{\n    return $query->with('tags');\n}\n",[65,1396,1397,1419,1423,1441],{"__ignoreMap":75},[79,1398,1399,1401,1403,1406,1408,1411,1414,1416],{"class":81,"line":82},[79,1400,1221],{"class":85},[79,1402,130],{"class":85},[79,1404,1405],{"class":89}," makeAllSearchableUsing",[79,1407,478],{"class":101},[79,1409,1410],{"class":110},"Builder",[79,1412,1413],{"class":101}," $query)",[79,1415,139],{"class":85},[79,1417,1418],{"class":110}," Builder\n",[79,1420,1421],{"class":81,"line":12},[79,1422,102],{"class":101},[79,1424,1425,1427,1430,1432,1435,1437,1439],{"class":81,"line":19},[79,1426,1240],{"class":85},[79,1428,1429],{"class":101}," $query",[79,1431,182],{"class":85},[79,1433,1434],{"class":89},"with",[79,1436,478],{"class":101},[79,1438,1180],{"class":163},[79,1440,862],{"class":101},[79,1442,1443],{"class":81,"line":117},[79,1444,295],{"class":101},[52,1446,1447],{},"And if the new field should be searchable, update the Scout config:",[70,1449,1451],{"className":72,"code":1450,"language":74,"meta":75,"style":75},"\u002F\u002F config\u002Fscout.php → typesense.model-settings\nOrder::class => [\n    'search-parameters' => [\n        'query_by' => 'number,customer_name,tags',\n    ],\n],\n",[65,1452,1453,1459,1471,1480,1492,1497],{"__ignoreMap":75},[79,1454,1455],{"class":81,"line":82},[79,1456,1458],{"class":1457},"sJ8bj","\u002F\u002F config\u002Fscout.php → typesense.model-settings\n",[79,1460,1461,1464,1467,1469],{"class":81,"line":12},[79,1462,1463],{"class":110},"Order",[79,1465,1466],{"class":85},"::class",[79,1468,167],{"class":85},[79,1470,157],{"class":101},[79,1472,1473,1476,1478],{"class":81,"line":19},[79,1474,1475],{"class":163},"    'search-parameters'",[79,1477,167],{"class":85},[79,1479,157],{"class":101},[79,1481,1482,1485,1487,1490],{"class":81,"line":117},[79,1483,1484],{"class":163},"        'query_by'",[79,1486,167],{"class":85},[79,1488,1489],{"class":163}," 'number,customer_name,tags'",[79,1491,1072],{"class":101},[79,1493,1494],{"class":81,"line":124},[79,1495,1496],{"class":101},"    ],\n",[79,1498,1499],{"class":81,"line":145},[79,1500,1501],{"class":101},"],\n",[59,1503,31],{"id":30},[52,1505,1506,1507,1511,1512,1515],{},"The schema PATCH is a ",[418,1508,1510],{"href":420,"rel":1509},[422],"synchronous blocking operation"," — incoming writes to the collection wait until it completes, but ",[306,1513,1514],{},"search queries continue without interruption",". For adding an optional field, this completes in milliseconds.",[52,1517,1518,1519,1521,1522,1525],{},"After the migration, the ",[65,1520,349],{}," field exists in the Typesense schema but no documents have values for it yet. Because the field is ",[65,1523,1524],{},"optional: true",", existing documents remain valid and searchable.",[52,1527,1528,1529,1532,1533,1535],{},"Documents get the new field naturally through Scout's model observer — whenever an order is saved and ",[65,1530,1531],{},"searchIndexShouldBeUpdated()"," returns true, Scout calls ",[65,1534,324],{}," and the new field is included. Over time, the index fills in without any bulk operation.",[52,1537,1538,1539,1542,1543,1545],{},"If you need the field populated faster for a subset of records, you can use Scout's ",[65,1540,1541],{},"searchable()"," method, which re-indexes models by calling ",[65,1544,324],{}," and upserting the result to Typesense:",[70,1547,1549],{"className":72,"code":1548,"language":74,"meta":75,"style":75},"Order::whereHas('tags')->searchable();\n",[65,1550,1551],{"__ignoreMap":75},[79,1552,1553,1555,1557,1560,1562,1564,1566,1568,1571],{"class":81,"line":82},[79,1554,1463],{"class":110},[79,1556,1015],{"class":85},[79,1558,1559],{"class":89},"whereHas",[79,1561,478],{"class":101},[79,1563,1180],{"class":163},[79,1565,1363],{"class":101},[79,1567,182],{"class":85},[79,1569,1570],{"class":89},"searchable",[79,1572,828],{"class":101},[52,1574,1575,1576,1581],{},"This re-indexes only the orders that actually have tags — not the entire collection. Typesense ",[418,1577,1580],{"href":1578,"rel":1579},"https:\u002F\u002Ftypesense.org\u002Fdocs\u002F27.1\u002Fapi\u002Fdocuments.html#configure-batch-size",[422],"interleaves imports and search queries"," (processing 40 documents, then servicing the search queue, then the next batch), so search stays responsive throughout.",[59,1583,34],{"id":33},[1585,1586,1587,1609],"table",{},[1588,1589,1590],"thead",{},[1591,1592,1593,1597,1600,1603,1606],"tr",{},[1594,1595,1596],"th",{},"Approach",[1594,1598,1599],{},"Downtime",[1594,1601,1602],{},"Data Loss Risk",[1594,1604,1605],{},"Deployment",[1594,1607,1608],{},"Rollback",[1610,1611,1612,1630],"tbody",{},[1591,1613,1614,1618,1621,1624,1627],{},[1615,1616,1617],"td",{},"Flush + import",[1615,1619,1620],{},"Minutes",[1615,1622,1623],{},"High (partial re-index)",[1615,1625,1626],{},"Manual",[1615,1628,1629],{},"Re-run flush + import",[1591,1631,1632,1635,1638,1641,1646],{},[1615,1633,1634],{},"Schema migration",[1615,1636,1637],{},"Reads unaffected; writes briefly blocked during PATCH",[1615,1639,1640],{},"None",[1615,1642,1643],{},[65,1644,1645],{},"migrate",[1615,1647,1648],{},[65,1649,1650],{},"migrate:rollback",[59,1652,37],{"id":36},[52,1654,1655,1657],{},[306,1656,1634],{}," works when you're:",[389,1659,1660,1663,1666],{},[303,1661,1662],{},"Adding optional fields",[303,1664,1665],{},"Removing fields",[303,1667,1668],{},"Changing field configuration (e.g., facet or index flags)",[52,1670,1671,1674],{},[306,1672,1673],{},"Flush and recreate"," is still necessary when you:",[389,1676,1677,1686,1689],{},[303,1678,1679,1680,1682,1683,1363],{},"Change a field's type (e.g., ",[65,1681,173],{}," → ",[65,1684,1685],{},"int32",[303,1687,1688],{},"Rename a field (Typesense doesn't support renames — add new, backfill, drop old)",[303,1690,1691],{},"Restructure the entire schema",[52,1693,1694],{},"For most day-to-day feature work — adding a new searchable field, making something filterable, indexing a new relationship — the migration approach keeps search running while you deploy.",[59,1696,40],{"id":39},[389,1698,1699,1705,1712],{},[303,1700,1701],{},[418,1702,1704],{"href":420,"rel":1703},[422],"Typesense: Update or Alter a Collection",[303,1706,1707],{},[418,1708,1711],{"href":1709,"rel":1710},"https:\u002F\u002Flaravel.com\u002Fdocs\u002Fscout",[422],"Laravel Scout Documentation",[303,1713,1714],{},[418,1715,1718],{"href":1716,"rel":1717},"https:\u002F\u002Fgithub.com\u002Ftypesense\u002Ftypesense-php",[422],"Typesense PHP Client",[1720,1721,1722],"style",{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":75,"searchDepth":12,"depth":12,"links":1724},[1725,1726,1730,1731,1732,1733,1734,1735],{"id":10,"depth":12,"text":11},{"id":14,"depth":12,"text":15,"children":1727},[1728,1729],{"id":17,"depth":19,"text":18},{"id":21,"depth":19,"text":22},{"id":24,"depth":12,"text":25},{"id":27,"depth":12,"text":28},{"id":30,"depth":12,"text":31},{"id":33,"depth":12,"text":34},{"id":36,"depth":12,"text":37},{"id":39,"depth":12,"text":40},"How to patch Typesense collections at deploy time using Laravel migrations instead of flushing and re-indexing.","md",false,{},null,"\u002Fblog\u002Ftypesense-schema-migrations-laravel",{"title":43,"description":1736},[1744,1745,1746],"LARAVEL","TYPESENSE","SCOUT","blog\u002Ftypesense-schema-migrations-laravel","BACKEND","4G53TYRgKCpjMpqaIE9LYEvSk5sFoix5eFvhOl6NeIA",1780311892629]