Requetes avec BQL
Interrogez les documents, la recherche vectorielle et la recherche par mot-cle avec une seule instruction SELECT, y compris les abonnements en direct et l'acces HTTP.
BQL est le langage de requete de barrel, un dialecte PartiQL (du SQL pour JSON) :
une seule instruction SELECT interroge les documents, la recherche vectorielle et
la recherche par mot-cle, avec des abonnements en direct. Lisez ceci quand vous
voulez interroger barrel avec une chaine de requete au lieu des maps de
specification structurees de find/2, ou quand vous voulez que les resultats
vectoriels et BM25 soient rattaches a leurs documents.
Quand l’utiliser
- Vous voulez une seule surface de requete : filtres, projections, recuperation top-k vectorielle ou par mot-cle, et mises a jour en direct avec la meme syntaxe.
- Vous pilotez barrel via HTTP et voulez envoyer une chaine de requete.
- Pour construire des requetes par programme, l’API de specification structuree
barrel:find/2reste de premier ordre ; BQL se compile sur le meme moteur.
Lancer une requete
{ok, Db} = barrel:open(mydb),
{ok, Rows, Meta} = barrel:query(Db,
<<"SELECT title, author.name AS who "
"FROM db "
"WHERE type = 'post' AND rank >= 3 "
"ORDER BY title LIMIT 10">>),
%% named parameters
{ok, Rows2, _} = barrel:query(Db,
<<"SELECT * FROM db WHERE org = $org">>,
#{params => #{<<"org">> => <<"acme">>}}).
Les lignes sont des maps : <<"id">> plus vos projections (SELECT * renvoie le
document entier aplati). Un attribut absent laisse sa cle absente de la ligne. Les
utilisateurs docdb embarques obtiennent les memes requetes de document a travers
barrel_docdb:query/2,3.
Les requetes sans ORDER BY, UNNEST ni LIMIT sont diffusees par morceaux : passez
chunk_size et suivez Meta (has_more, continuation), ou repliez sans
materialiser :
{ok, Count, _} = barrel:query_fold(Db,
<<"SELECT * FROM db WHERE type = 'post'">>, #{chunk_size => 100},
fun(_Row, N) -> {ok, N + 1} end, 0).
Le langage
SELECT d.title, d.author.name AS who
FROM db AS d
WHERE d.type = 'post' AND (d.rank > 3 OR d.pinned = true)
ORDER BY d.title DESC
LIMIT 10 OFFSET 20
- Le nom du FROM est la variable de portee ; la base de donnees vient de l’appel
d’API.
AS dest optionnel ; sans lui le nom du FROM est l’alias. - Chemins :
d.a.b, index de tableaud.tags[0], cles entre guillemetsd."a key". Les mots-cles sont acceptes apres un point (d.order). Les champs de premier niveau prefixes par_sont reserves et rejetes. - Operateurs :
=,!=,<,<=,>,>=,IN (..),LIKE,BETWEEN a AND b,IS [NOT] NULL,IS [NOT] MISSING,CONTAINS(path, value),AND,OR,NOT. Les comparaisons ne correspondent qu’entre valeurs de meme type (deux nombres ou deux chaines). - Les parametres
$namelient des scalaires depuis la mapparams. WHERE id = 'x', les plages d’id, etid LIKE 'prefix%'deviennent des balayages de cle primaire.- ORDER BY prend une seule cle ; les valeurs absentes se trient apres les nombres et les chaines.
L’egalite, les plages, LIKE 'prefix%', IS NOT MISSING, et leurs combinaisons
AND utilisent les index de chemin. OR, IN, NOT, LIKE en forme d’expression
reguliere, IS NULL, IS MISSING et CONTAINS font un balayage ;
explain_query liste un avertissement {full_scan, _} pour chacun :
{ok, #{engine := #{strategy := Strategy}, warnings := Warnings}} =
barrel:explain_query(Db, <<"SELECT * FROM db WHERE a = 1 OR b = 2">>).
MISSING contre NULL
PartiQL distingue un null stocke d’un attribut absent :
a IS MISSING: l’attribut est absent.a IS NULL: l’attribut estnullOU absent.a = NULLne correspond jamais et est rejete ; utilisez les formes ci-dessus.
UNNEST des tableaux
UNNEST produit une ligne par element de tableau ; l’element obtient son propre
alias, utilisable dans WHERE, SELECT et ORDER BY. Les valeurs vides, absentes ou
non-tableaux ne produisent aucune ligne.
SELECT d.title, t AS tag
FROM db AS d, UNNEST(d.tags) AS t
WHERE d.type = 'post' AND t = 'erlang'
Les predicats sur l’element sont evalues par ligne ; les predicats sur le document lui-meme utilisent toujours les index.
Fonctions de table de recherche
La recherche vectorielle et par mot-cle entrent dans le langage comme sources
FROM. L’alias est requis ; chaque fonction prend une chaine de requete (ou un
$param) et des options nommees.
SELECT v._score, title FROM vector_top_k('rust orm', k => 10) AS v
SELECT m._score, title FROM bm25_top_k('rust', k => 10) AS m
SELECT h._score, title FROM hybrid_top_k('rust orm', k => 10) AS h
WHERE h.lang = 'en'
kvaut 10 par defaut ;vector_top_kprend aussief_search.hybrid_top_kfusionne les branches vectorielle et BM25 avec RRF.- Les resultats sont rattaches a leurs documents par id ; les resultats dont le document a disparu sont ecartes. Sur les bases de donnees en mode enregistrement, l’embedding du texte de requete est calcule par l’embedder de la base de donnees.
_scoreest specifique a la fonction (1 - distance, BM25 brut, fusion RRF) et n’est pas comparable entre fonctions. Les lignes devector_top_kportent aussi_distance. Les deux sont uniquement des colonnes SELECT et ORDER BY.- Un WHERE filtre les resultats apres recuperation : barrel sur-recupere une fois
(3x
k, plafonne a 1000) et renvoie JUSQU’Aklignes ; un filtrage lourd peut en renvoyer moins. L’ordre par defaut est le rang de recherche.
Requetes en direct
Ajoutez SUBSCRIBE et abonnez-vous au lieu de lancer : vous obtenez
l’instantane, puis des deltas d’ajout/modification/retrait a mesure que des
documents commencent ou cessent de correspondre.
{ok, Sub} = barrel:subscribe_query(Db,
<<"SELECT name FROM db WHERE status = 'active' SUBSCRIBE">>),
#{ref := Ref} = Sub,
receive {bql_rows, Ref, Rows} -> Rows end,
receive {bql_ready, Ref, #{count := N}} -> N end,
%% then, per matching write:
%% {bql_change, Ref, #{action := add | change, id, rev, row}}
%% {bql_change, Ref, #{action := remove, id}}
ok = barrel:unsubscribe_query(Sub).
- LIMIT ne plafonne que l’instantane initial ; les deltas sont non bornes.
- Les fonctions de table, UNNEST, ORDER BY et OFFSET ne se combinent pas avec SUBSCRIBE.
- La requete s’arrete quand vous vous desabonnez ou quand le processus proprietaire meurt. La detection des retraits suit le flux de changements, attendez-la donc dans son intervalle de polling (environ 100 ms).
Via HTTP
POST /db/:db/query prend le texte BQL comme corps (ou du JSON
{"query", "params", "continuation"}) et diffuse du ndjson : une ligne
{"row": ...} par ligne, puis une ligne {"meta": ...} avec has_more et un
jeton continuation a renvoyer par POST. Les erreurs de requete sont un 400 avec
message, line et column.
curl -s http://localhost:8080/db/mydb/query \
-d "SELECT title FROM db WHERE type = 'post' LIMIT 3"
{"row":{"id":"post:1","title":"..."}}
{"row":{"id":"post:2","title":"..."}}
{"row":{"id":"post:3","title":"..."}}
{"meta":{"has_more":false}}
Les instructions SUBSCRIBE necessitent Accept: text/event-stream (ou un
EventSource de navigateur sur GET /db/:db/query?q=...) et diffusent des
evenements row, ready, change et error avec un ping periodique.
Notes
- Portee v1 : SELECT, WHERE, ORDER BY (une cle), LIMIT/OFFSET, UNNEST (un), les
trois fonctions de table, SUBSCRIBE. Pas de jointures, pas de GROUP BY, pas de
chemins joker
[*](utilisez UNNEST). - ORDER BY, UNNEST et LIMIT/OFFSET materialisent leur resultat (borne) avant de repondre ; les filtres simples sont diffuses en flux.
- Un
SELECTde projections etroites lit quand meme les documents entiers ; la projection a lieu apres la recuperation.