Quickstart0.1.0
Luxir has a JSON API over HTTP. This page starts the server, indexes a few
documents with curl, and searches them. No schema, client library, or
cluster setup is needed first.
Get Luxir
Section titled “Get Luxir”Download the single standalone executable for your platform from https://luxir.org/download/. No unpacking is needed.
curl -fL https://github.com/luxir-search/luxir/releases/download/v0.1.0/luxir-0.1.0-linux-x86_64-v2 -o luxirchmod +x luxir./luxir --versionStart the server
Section titled “Start the server”./luxirThat’s it. The HTTP/JSON API is listening on port 9400, storing data in
memory. To keep data across restarts, point it at a directory:
./luxir --store.backend=fs --store.data-dir=./dataCheck it’s alive:
curl http://localhost:9400/healthIndex documents
Section titled “Index documents”Index three books with sample prices in dollars:
curl -X POST http://localhost:9400/collections/books/_update \ -H 'Content-Type: application/json' \ -d '{ "docs": [ { "id": "1", "title_t": "The Way of Kings", "author_name": "Brandon Sanderson", "series_s": "Stormlight", "year_i": 2010, "price_f": 12.5 }, { "id": "2", "title_t": "Words of Radiance", "author_name": "Brandon Sanderson", "series_s": "Stormlight", "year_i": 2014, "price_f": 15.0 }, { "id": "3", "title_t": "Mistborn: The Final Empire", "author_name": "Brandon Sanderson", "series_s": "Mistborn", "year_i": 2006, "price_f": 8.5 } ], "commit": {} }'You should get a response that looks something like the following:
{"update_version":1,"status":"ok"}The write created the books collection, and built-in field templates
supplied the field types: _t is searchable text, _name is a name that
supports word search plus whole-name facets and sorting, _s is an exact
string, _i is an integer you can range and sort on, and _f stores
floating-point numbers. You did not need to define your own schema.
Templates also handle fields you do not know about yet. For example, a custom
_attr template can cover new product attributes as an ecommerce catalog
grows. You can combine templates with explicit field definitions and choose
your own names. See Schema.
author_name is one input indexed two ways: author_name:sanderson matches a
word of the name, while a facet or sort on author_name uses the whole value
Brandon Sanderson. Field variants shows how
the template does this and how to define your own.
Search
Section titled “Search”The simplest search is a URL:
curl 'http://localhost:9400/collections/books/_search?pretty&query=title_t:kings'{ "docs": [ { "id": "1", "author_name": "Brandon Sanderson", "price_f": 12.5, "series_s": "Stormlight", "title_t": "The Way of Kings", "year_i": 2010 } ]}title_t:kings searches the title field for kings, using the same text
analysis as indexing, so it finds “The Way of Kings”. query is an
expression in the Luxir query language. pretty formats
the response for reading; leave it off and a program gets one compact line.
fields, sort, limit, and the other common request fields have
URL parameter forms too.
Requests with more structure, such as facets and metrics, are JSON bodies. The rest of this page uses them.
Reading the rest of this page: requests are shown as HTTP: method, path, and body. On the website, every request block has a Copy as curl button that copies the runnable command, with
?prettyadded to the URL so the output reads well in a terminal. If you are reading the Markdown source, wrap one yourself as above:curl -X POST 'http://localhost:9400<path>?pretty' -H 'Content-Type: application/json' -d '<body>'.
The same search as JSON, choosing which fields come back and asking for the exact match count:
{ "query": "title_t:kings", "fields": ["id", "title_t", "author_name", "year_i"], "get_number": true}{ "found": 1, "docs": [ { "id": "1", "title_t": "The Way of Kings", "author_name": "Brandon Sanderson", "year_i": 2010 } ]}The equivalent structured form of the query is
"query": {"match": {"title_t": "kings"}}. You can use either form anywhere
a query is accepted.
fields chooses what comes back; by default, every retrievable field is
returned.
To get the same keys in every doc, add "document_format": "columns" to the
request. Every supported projected field then appears in every doc, with an
explicit null where the document has no value, which is convenient when
feeding rows into a table. (Over gRPC, responses are natively columnar; this
setting picks the placement there too.)
Counts are exact
Section titled “Counts are exact”Add get_number and found is the total number of matching documents,
regardless of limit:
{ "query": {"match": {"author_name": "sanderson"}}, "fields": ["id", "title_t"], "get_number": true, "limit": 2}{ "found": 3, "docs": [ { "id": "1", "title_t": "The Way of Kings" }, { "id": "2", "title_t": "Words of Radiance" } ]}found is 3 even though limit was 2.
Forgiving end-user search
Section titled “Forgiving end-user search”For a search box where a human types whatever they want, use simple_query.
It understands operators, quotes, and field terms, and it never returns a
parse error, so malformed input still runs as a search:
{ "query": { "simple_query": {"q": "kings | radiance", "fields": ["title_t"]} }, "fields": ["id", "title_t"], "get_number": true}{ "found": 2, "docs": [ { "id": "2", "title_t": "Words of Radiance" }, { "id": "1", "title_t": "The Way of Kings" } ]}The query language
Section titled “The query language”When you’re the one writing the query, a bare string anywhere a query object goes is an expression in the Luxir query language: fielded terms, AND/OR/NOT, ranges, and function forms for most structured query types:
{ "query": "title_t:(kings OR radiance) AND year_i:[2010 TO 2013]", "fields": ["id", "title_t"], "get_number": true}{ "found": 1, "docs": [ { "id": "1", "title_t": "The Way of Kings" } ]}“Words of Radiance” matched the title group, but its year_i is 2014,
outside the requested range of 2010 through 2013.
Facets and metrics, in the same request
Section titled “Facets and metrics, in the same request”The same request can return books, count them by series, and calculate their average price. This example also includes the lowest price per series:
{ "query": { "match": { "author_name": "sanderson" } }, "fields": ["id", "title_t", "price_f"], "limit": 2, "get_number": true, "ops": { "series": { "field_facet": { "field": "series_s", "ops": { "lowest_price": "min(price_f)" } } }, "average_price": "avg(price_f)" }}{ "found": 3, "docs": [ { "id": "1", "title_t": "The Way of Kings", "price_f": 12.5 }, { "id": "2", "title_t": "Words of Radiance", "price_f": 15 } ], "ops": { "average_price": 12, "series": { "buckets": [ { "val": "Stormlight", "count": 2, "lowest_price": 12.5 }, { "val": "Mistborn", "count": 1, "lowest_price": 8.5 } ] } }}Operations nest: lowest_price runs once per series bucket, while
average_price summarizes all three matches, including the book beyond
limit: 2. One round trip returns the documents, facet counts, and aggregate
metrics over one consistent view of the index.
Faceting covers range and date buckets, nested facets, and top
documents per bucket.
Bulk ingest: stream a whole file
Section titled “Bulk ingest: stream a whole file”Set the content type to application/x-ndjson and send one document per line.
The stream is unbounded - pipe in a file of any size and Luxir indexes it as it
arrives, without buffering the whole thing. ?commit=true commits at the end
of the stream, making the documents searchable before the request completes:
{"id": "4", "title_t": "Oathbringer", "author_name": "Brandon Sanderson", "series_s": "Stormlight", "year_i": 2017, "price_f": 17.5}{"id": "5", "title_t": "The Well of Ascension", "author_name": "Brandon Sanderson", "series_s": "Mistborn", "year_i": 2007, "price_f": 10.0}{"update_version":2,"status":"ok"}To index an NDJSON file you already have:
curl -X POST 'http://localhost:9400/collections/books/_update?commit=true' \ -H 'Content-Type: application/x-ndjson' \ --data-binary @books.ndjsonThe optional _update_ and _end_ control records let you group updates and
set options within a stream. See Indexing.
Bulk export
Section titled “Bulk export”Add ?format=docs to a query and the response is NDJSON, one document per
line with no envelope. limit: -1 returns every match, streamed over one
connection, so there is no scroll API or cursor token to manage:
{"query": {"all": true}, "limit": -1, "fields": ["id", "title_t"]}{"id":"1","title_t":"The Way of Kings"}{"id":"2","title_t":"Words of Radiance"}{"id":"3","title_t":"Mistborn: The Final Empire"}{"id":"4","title_t":"Oathbringer"}{"id":"5","title_t":"The Well of Ascension"}With get_number, a _header_ line starts the stream so a consumer knows
the total before reading the documents: {"_header_":{"found":5}}.
Execution warnings, when there are any, also arrive in a _header_ line.
Ingest recognizes and skips header lines, so an export can be piped straight
back into /_update:
curl -s 'http://localhost:9400/collections/books/_search?format=docs' \ -H 'Content-Type: application/json' \ -d '{"query": {"all": true}, "limit": -1, "fields": ["id", "title_t"]}' |curl -X POST 'http://localhost:9400/collections/backup/_update?commit=true' \ -H 'Content-Type: application/x-ndjson' --data-binary @-Multiple collections
Section titled “Multiple collections”Collections do not need to be created in advance. The first update to a new collection name creates it:
{"docs": [{"id": "a", "title_t": "The Good, the Bad and the Ugly"}], "commit": {}}{"update_version":1,"status":"ok"}{ "query": {"match": {"title_t": "ugly"}}, "fields": ["id", "title_t"], "get_number": true}{ "found": 1, "docs": [ { "id": "a", "title_t": "The Good, the Bad and the Ugly" } ]}The same server holds multiple collections as independent index namespaces.
Auto-create is on by default; set --no-indexing.auto-create-collection if you don’t want it.
Committing
Section titled “Committing”Changes become visible on commit. You have three ways, use whichever fits:
- In a JSON update body:
"commit": {}. - On the request URL, JSON or NDJSON:
POST /collections/books/_update?commit=true. - At the end of a stream:
{"_end_": {"commit": {}}}.
See what the server understood
Section titled “See what the server understood”Add ?explain=request to a query and Luxir echoes back the canonical request it
parsed - the shorthand you sent, expanded to the full form:
{"query": {"match": {"title_t": "kings"}}}{ "collection": "books", "ops": { "q": { "top_docs": { "query": { "match": { "field": "title_t", "val": "kings" } } } } }}This is useful for learning the API and for debugging a query that isn’t matching what you expect: type the short form, read back the full one.
Use ?explain=resolved to inspect which physical
fields a request uses. It returns request and resolved_fields, and runs
ordinary preparation without collecting results. Post back the request
member to execute it.
Where to go next
Section titled “Where to go next”- Documents and values - field naming, IDs, nulls, arrays, and coercion.
- Indexing - update, delete, commit, error, and stream semantics.
- Searching - request and response shapes, filters, ops, and metrics.
- Structured queries - every query arm and option.
- Faceting - field facets, range facets, and nested analytics.
- Vector search - dense-vector and hybrid retrieval.
- Geo search - bounding boxes and distance queries.
- HTTP conventions - framing, validation, and error behavior.
- Operating Luxir - persistence, resource controls, and security.