Ningú no discuteix que un motor de cerca va al costat de la base de dades, no al seu lloc. És la resposta correcta el noranta per cent de les vegades, i per això gairebé ningú no mira el deu per cent restant, on aquesta segona còpia només genera feina. Aquí veuràs on és exactament aquesta línia, què hi guanyes i a què renuncies en creuar-la. Els exemples van en Laravel perquè és una de les nostres especialitats a AndorraDev, però el raonament s'aplica igual amb qualsevol framework i qualsevol motor de cerca.
Un índex de cerca és una còpia, i les còpies es desincronitzen
El muntatge habitual és aquest: les dades viuen a la base de dades relacional i un motor de cerca en manté una còpia optimitzada per consultar.
Funciona, i té un cost que gairebé mai es comptabilitza a l'hora de decidir-ho. Cal escriure una ordre de sincronització, mantenir un treballador de cua per a les actualitzacions, vigilar la desviació que apareix quan una tasca falla i tenir a punt un guió de reindexació completa per al dia que canviï el model d'embeddings.
Tot això és infraestructura que no aporta funcionalitat. Existeix només perquè dues còpies diguin el mateix.
Per a la majoria de les dades és un preu just, perquè la base de dades aporta transaccions, relacions i integritat que el motor de cerca no té. Però hi ha una categoria de dades on no aporta res.
Els fragments de text no tenen vida pròpia al teu esquema relacional
Pensa en el que passa quan trosseges un document llarg per poder cercar-hi per significat.
Agafes una llei, la parteixes en articles, cada article en passatges d'uns centenars de paraules, calcules l'embedding de cada passatge i el guardes. El mateix amb documentació, amb transcripcions de reunions o amb un històric de tiquets de suport.
Aquests fragments no són entitats del teu negoci. No tenen relacions a respectar, ningú els edita d'un en un, no participen en cap transacció i la seva clau primària és un identificador que t'has inventat en trossejar. Existeixen per un únic motiu: per ser trobats.
Ficar-los en una taula relacional i després copiar-los al motor de cerca és pagar el preu complet de la sincronització a canvi de garanties que aquella dada no necessita.
El que hi guanyes és no haver de sincronitzar res
Si l'índex és la font de veritat, la segona còpia desapareix i amb ella tot el que es va escriure per mantenir-la.
Ja no hi ha ordre de sincronització, ni cua, ni desviació possible. Reindexar deixa de ser "buidar i tornar a copiar des de SQL" i passa a ser senzillament tornar a generar els fragments.
I hi ha un estalvi en temps d'execució que es nota més del que sembla. Un cercador que emmiralla una taula retorna identificadors i després ha d'anar a la base de dades a rehidratar els resultats, amb una consulta per cerca. Si el document de l'índex ja porta tots els camps, aquesta consulta no existeix: zero consultes SQL per cerca.
Quines dades aguanten aquest tracte i quines no
Aquesta és la part que decideix, i convé tenir-la al davant abans de moure res. Un motor de cerca és un motor de cerca: dona recuperació per rellevància, no les garanties d'un motor relacional.
Les transaccions es queden a la teva base de dades. Guardar un fragment i actualitzar un comptador en la mateixa operació atòmica és cosa del SQL.
Les relacions i les claus foranes també. Res no impedeix que un fragment apunti a un document que ja no existeix, així que la integritat la manté el teu procés de trossejat.
I les restriccions igual: unicitat, no nuls i comprovacions viuen on viu la dada canònica.
I sobretot: si perds l'índex, perds les dades. Amb un índex mirall, reconstruir-lo és una ordre. Aquí és una restauració de còpia de seguretat com la de qualsevol altra base de dades, amb la qual cosa l'índex passa a necessitar la mateixa política de còpies que el teu SQL.
Per a fragments de text cap d'aquestes pèrdues fa mal. Per a comandes i factures farien mal totes. La línia és aquí, i és força nítida.
Abans de moure un tipus de dada a l'índex, fes-te una pregunta: si demà perdés això, ho podria regenerar? Els fragments d'un document sí, perquè el document original continua al seu lloc. Una comanda no. Aquesta pregunta separa millor els dos casos que qualsevol discussió sobre rendiment, i a més et diu si necessites còpies de seguretat de l'índex o en tens prou de poder-lo reconstruir.
L'API que necessites per tractar un índex com una taula
Si has de fer això, el que vols és escriure codi que s'assembli al que ja escrius, no crides crues al SDK repartides per tota l'aplicació.
La configuració de l'índex es declara a la mateixa classe, que és on es pot llegir i versionar:
class LawChunk extends Meili
{
protected static string $index = 'law_chunks';
protected static string $primaryKey = 'external_id';
protected static array $searchable = ['content', 'title_path'];
protected static array $filterable = ['law_id', 'country', 'date'];
protected static array $sortable = ['position'];
}
I després una ordre empeny aquests ajustos al servidor, amb la qual cosa la configuració del motor deixa de ser una cosa que algú va tocar un dia en un tauler i passa a estar al repositori com la resta del codi.
Les consultes es compilen als paràmetres que el motor entén, i la cerca híbrida és de primera classe:
LawChunk::query()
->where('country', 'AD')
->semantic(0.7)
->limit(20)
->get();
Cal assumir que el llenguatge de consulta és més pobre que el de SQL, i no per una limitació de l'embolcall sinó del motor: els filtres s'encadenen amb I lògic i per a una disjunció cal baixar a l'expressió crua. No hi ha agregats. No hi ha unions. És un cercador, no un motor relacional.
El que sí que es pot resoldre bé és la unió amb la teva base de dades real. Un fragment pot tenir una relació amb un model Eloquent, i en carregar una llista de resultats aquesta relació es resol amb una sola consulta per a tots, no una per fragment.
L'escotilla de sortida importa tant com l'abstracció
Tota capa còmoda sobre un servei extern cobreix el vuitanta per cent dels casos i amaga la resta. El que decideix si aquesta capa et serveix al cap de dos anys no és què bé embolcalla l'habitual, és com de fàcil resulta saltar-se-la el dia que necessites una cosa que no va preveure.
Un cercador seriós té funcionalitat que cap capa fluida cobrirà sencera. Quan vols demanar la puntuació de rellevància de cada resultat per reordenar-los tu, o passar-li un vector que ja has calculat pel teu compte en lloc de deixar que el calculi ell, necessites el client cru.
La manera correcta de resoldre-ho no és ampliar l'abstracció fins a cobrir-ho tot, és deixar el client accessible:
LawChunk::client()
->index(LawChunk::indexName())
->search($query, $options)
->getHits();
Allà continues tenint el nom de l'índex i la configuració en un sol lloc, i alhora tens el SDK sencer disponible per al cas rar. A Crowd Legal el cercador de legislació fa servir exactament aquesta via, perquè calcula els seus propis embeddings i reordena els resultats agregant la puntuació per document abans de retornar-los.
Una abstracció que t'obliga a triar entre fer-la servir del tot o no fer-la servir és una abstracció que acabaràs traient.
Scout i això no competeixen
Així que se sent Meilisearch i Laravel a la mateixa frase, la resposta automàtica és Scout. Val la pena aturar-s'hi perquè totes dues coses semblen resoldre el mateix i en realitat tenen fonts de veritat diferents, que és la diferència que importa.
Laravel Scout fa una cosa diferent: manté un índex mirall d'una taula Eloquent. La seva font de veritat és la taula, i l'índex és un derivat que es pot reconstruir quan vulguis. Per al catàleg de productes, per als clients o per a qualsevol cosa que a més viu al teu esquema, és l'eina correcta i no hi ha motiu per canviar-la: et dona observers, cua i importació massiva sense escriure res.
El que resol tractar l'índex com a magatzem primari és l'altre cas, el de la dada que només existeix per ser trobada. I les dues coses conviuen al mateix projecte sense problema: comparteixen la mateixa connexió al servidor i no es destorben.
Dit d'una altra manera: si et preguntes "on viu això de veritat?", i la resposta és "a la meva base de dades", fes servir Scout. Si la resposta és "enlloc, ho genero en trossejar un document", aleshores la taula intermèdia només t'està donant feina.
I si no treballes amb Laravel
El criteri és del disseny, no del framework:
- Pregunta't si la dada és regenerable. Si ho és i només serveix per cercar, la còpia relacional probablement sobra.
- Declara la configuració de l'índex al codi, no en un tauler. Els ajustos d'un motor de cerca són configuració d'aplicació i mereixen estar al control de versions.
- Guarda al document tot el que hagis de mostrar. Si has d'anar a la base de dades a completar els resultats, has tornat al punt de partida.
- I exigeix una escotilla de sortida. Qualsevol capa sobre un servei extern t'ha de deixar arribar al client cru, perquè el dia que necessitis un paràmetre que no cobreix, el necessitaràs de debò.
On és el codi
El paquet és a Packagist, el codi a GitHub i la documentació del paquet recull l'API completa. Són unes mil línies, sense migracions i sense taules, i reutilitza la mateixa configuració de connexió que ja tinguis posada per a Scout.
És en producció a Crowd Legal, on els fragments de legislació i de jurisprudència viuen a l'índex i es recuperen per significat.
Si estàs muntant cerca semàntica sobre els teus propis documents, aquesta decisió d'arquitectura és la primera i condiciona la resta. La treballem a intel·ligència artificial i software legal, i si ens expliques què indexaràs et diem on posar la línia.