For AI agents: this page is also available as Markdown at /docs/sql.md. Every docs page is: add .md to its URL. llms.txt lists them all.
SQL and PostgreSQL
You can query a collection with a single SQL SELECT. The FROM clause names the collection, WHERE filters and searches, ORDER BY sorts, and GROUP BY turns into facets. Two dialects are accepted:
language | Dialect | Adds |
|---|---|---|
sql | MySQL | MATCH ... AGAINST and related full-text functions |
pgsql | PostgreSQL | tsvector / tsquery full-text search, pgvector distance operators, casts, date_trunc |
curl -X POST https://api.warpsearch.io/v1/search \
-H "Authorization: Bearer $WARPSEARCH_API_KEY" \
-H "Content-Type: application/json" \
--data @- <<'EOF'
{"language": "pgsql",
"query": "SELECT title, year FROM books WHERE to_tsvector(title) @@ websearch_to_tsquery('dune -messiah') ORDER BY year DESC LIMIT 10"}
EOF
SQL is accepted wherever a search is: POST /v1/search, POST /v1/collections/{c}/search, GET /v1/collections/{c}/search?language=pgsql&q=..., POST /v1/collections/{c}/count, /v1/translate, and the MCP search, count and translate_query tools. When the request also names a collection, it must match FROM.
There is no PostgreSQL or MySQL wire protocol. psql, JDBC and ODBC drivers, BI tools and Grafana's PostgreSQL data source cannot connect. Send the query over HTTP or MCP.
#What a query returns
The response is the same as for any other search: hits, facets, metrics and total.
- The
SELECTcolumns become the returned fields.SELECT *returns every field you are allowed to see. - A query with only aggregates, such as
SELECT COUNT(*), AVG(price) FROM products WHERE ..., returns no hits, an exacttotaland the aggregates undermetrics. GROUP BYreturns groups as facet buckets, with the aggregates as metrics of each bucket.
SELECT category, COUNT(*), AVG(price) AS avg_price FROM products
WHERE MATCH(title) AGAINST('running')
GROUP BY category
{"total": 3, "hits": [],
"facets": {"category": {"type": "terms", "field": "category", "buckets": [
{"value": "shoes", "count": 2, "metrics": {"avg_price": 104.75}},
{"value": "apparel", "count": 1, "metrics": {"avg_price": 12}}]}}}
#Both dialects
| Feature | Notes |
|---|---|
=, <> / !=, <, <=, >, >= | On a text field = is a phrase match. On string, number and date fields it is exact. |
IN, NOT IN, BETWEEN, IS [NOT] NULL, IS [NOT] TRUE / FALSE, AND, OR, NOT | A bare boolean column means col = true. col = NULL matches nothing, as in SQL. |
LIKE, NOT LIKE, ILIKE, with ESCAPE | 'abc%' is a prefix query. Other patterns are wildcard queries (% and _), which depend on your plan. ILIKE matches case-sensitively, with a warning; to match a string field case-insensitively, give it a lowercase normalizer. |
COUNT(*), COUNT(col), AVG, SUM, MIN, MAX | |
GROUP BY a, b | Nested terms facets, 10 groups per level by default. LIMIT sets the number of groups at the first level. |
HAVING COUNT(*) >= n | Only COUNT(*) with > or >=. |
ORDER BY | Columns, aliases and _score. With GROUP BY, an aggregate or its alias orders the groups. The default is by count, descending. |
SELECT DISTINCT col | One column, returned as a terms facet. |
LIMIT n OFFSET m | OFFSET cannot be combined with GROUP BY. |
Regular expressions and wildcard patterns depend on your plan and answer 403 forbidden when it does not include them. See Limits and errors.
#PostgreSQL (pgsql)
SELECT title, year FROM books
WHERE to_tsvector(title) @@ to_tsquery('dune & !messiah')
AND author ILIKE 'frank%' AND year BETWEEN 1960 AND 1980
AND genre = ANY(ARRAY['scifi', 'fantasy'])
ORDER BY ts_rank(title, to_tsquery('dune')) DESC LIMIT 10 OFFSET 20
#Full-text search
| PostgreSQL | What it does |
|---|---|
to_tsvector(col) @@ to_tsquery('a & b | !c') | & requires a term, | makes it optional, ! excludes it. Parentheses group. term:* is a prefix and a <-> b a phrase. |
plainto_tsquery('a b') | All words must match. |
phraseto_tsquery('a b') | The words as a phrase. |
websearch_to_tsquery('"a b" -c or d') | Quoted phrases, - to exclude, or between alternatives. |
col @@ 'query', 'query'::tsquery | Same as to_tsquery on that column. |
to_tsvector(a || ' ' || b) | Search several fields at once. coalesce(...) around a column is accepted. |
ts_rank(...), ts_rank_cd(...) | The relevance score, which is BM25. In ORDER BY it sorts by score. |
Each field is searched with its own analyzer. A text search configuration such as 'english' is ignored with a warning, and so are weights (setweight, :A). Inside a <-> phrase only plain terms are allowed, and <N> distances other than <-> are not supported.
#Vector search with pgvector operators
ORDER BY a distance and LIMIT k run a nearest-neighbor search on a vector field:
SELECT id, title FROM docs
WHERE lang = 'en' AND embedding <=> '[0.12, -0.03, 0.44]' < 0.4
ORDER BY embedding <=> '[0.12, -0.03, 0.44]'
LIMIT 10
<->is L2 distance,<=>cosine distance and<#>negative inner product. The operator must match the field'smetric; a mismatch is an error. The L1 operator<+>is not supported.- Vectors can be written
'[1,2,3]',ARRAY[1,2,3]or'[1,2,3]'::vector. - k is
LIMITplusOFFSET, 10 when there is noLIMIT. - Every
ANDcondition inWHEREfilters inside the vector search, so you get k matching results rather than k results filtered afterwards. - A bound such as
embedding <=> v < 0.4, on the same field, operator and vector as theORDER BY, drops neighbors beyond that distance. It is converted to a similarity threshold. - The distance can be selected (
embedding <-> v AS dist), but the value returned is the hit's similarity score, not the distance. A warning says so.
The distance must be the only ORDER BY key, ascending. It cannot be combined with GROUP BY or aggregates, and a distance cannot be used in WHERE without the matching ORDER BY. See Vector search for indexes, recall and filters.
#Other PostgreSQL features
| Feature | Notes |
|---|---|
~, ~*, !~, !~* | Regular expressions. ~* matches case-sensitively, with a warning. |
= ANY(ARRAY[...]), = ANY('{a,b}'), <> ALL(...) | Like IN and NOT IN. |
BETWEEN SYMMETRIC, ISNULL, NOTNULL, IS UNKNOWN | |
'2024-01-01'::date, CAST(x AS int) | Casts convert literals. A cast on a column is ignored with a warning. |
now(), current_timestamp, current_date, date_trunc('day', now()) | |
now() - interval '7 days', '7 days'::interval | One unit per interval. '1 day 2 hours' is not supported; write '26 hours'. |
GROUP BY date_trunc('day', ts) | A date histogram. Units: second, minute, hour, day, week, month, quarter, year. Only as the first GROUP BY key, and LIMIT does not apply to it. |
COUNT(DISTINCT col) | Approximate (HyperLogLog, exact below about 3,000 values), with a warning. |
FETCH FIRST n ROWS ONLY, LIMIT ALL, NULLS LAST | NULLS FIRST is ignored with a warning; missing values sort last. |
"Quoted" identifiers, table aliases, ORDER BY 1 | Unquoted identifiers are lowercased, as in PostgreSQL. |
#MySQL (sql)
SELECT title, year FROM books
WHERE year BETWEEN 1960 AND 1980 AND genre IN ('scifi', 'fantasy')
AND MATCH(title) AGAINST('dune') AND author LIKE 'Frank%'
ORDER BY year DESC LIMIT 10 OFFSET 20
| Feature | Notes |
|---|---|
MATCH(col, ...) AGAINST('words') | Matches the words in any of the columns. |
MATCH(...) AGAINST('...' IN BOOLEAN MODE) | +, - and quotes, like a simple query string. |
match_query(col, 'words'), match_phrase(col, 'exact words') | |
query_string('title:dune AND year:>1960') | Lucene syntax over the default fields. Also simple_query_string. |
score() | The relevance score, in SELECT or ORDER BY. |
REGEXP, NOT REGEXP | Regular expressions. |
<=> | Null-safe equals. col <=> NULL matches documents without the field. |
NOW(), CURDATE(), NOW() - INTERVAL 7 DAY, DATE_SUB(...) | Date math. ISO-8601 strings compare as dates. |
COUNT(DISTINCT ...) is not supported in sql; use pgsql for an approximate count.
#Not supported
These return 400:
- Joins, subqueries,
WITH(CTEs),UNION,INTERSECT,EXCEPT, window functions,ROLLUPandCUBE. - Functions or arithmetic on columns (
lower(title) = ...,price * 2),CASE, and comparing two columns. - Aggregates other than
COUNT,AVG,SUM,MINandMAX, such aspercentile_cont,array_aggandstring_agg. DISTINCT ON,SIMILAR TO,IS DISTINCT FROM,ts_headline,ORDER BY random().$1parameters and prepared statements. Inline the values, and note that dollar-quoted strings ($$...$$) are not supported either.- Anything but
SELECT: noINSERT,UPDATE,DELETE, transactions or DDL. Create collections and write documents through the REST API. - Schema-qualified names such as
public.books.
The same query in other languages, and how to see what WarpSearch made of yours, are on Query languages.