Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hands-on Tutorial

This tutorial walks you through a complete workflow with laurus-server: starting the server, creating an index, adding documents, searching, updating, and deleting. All examples use curl via the HTTP Gateway.

Prerequisites

  • laurus CLI installed (see Installation)
  • curl available on your system

Step 1: Start the Server

Start laurus-server with the HTTP Gateway enabled:

laurus --index-dir /tmp/laurus/tutorial serve --port 50051 --http-port 8080

You should see log output indicating the gRPC server (port 50051) and the HTTP Gateway (port 8080) have started.

Verify the server is running:

curl http://localhost:8080/v1/health

Expected response:

{"status":"SERVING_STATUS_SERVING"}

Step 2: Create an Index

Create an index with a schema that defines text fields for lexical search and a vector field for vector search. This example demonstrates custom analyzers, embedder definitions, and per-field configuration:

curl -X POST http://localhost:8080/v1/index \
  -H 'Content-Type: application/json' \
  -d '{
    "schema": {
      "analyzers": {
        "body_analyzer": {
          "char_filters": [{"type": "unicode_normalization", "form": "nfkc"}],
          "tokenizer": {"type": "regex"},
          "token_filters": [
            {"type": "lowercase"},
            {"type": "stop", "words": ["the", "a", "an", "is", "it"]}
          ]
        }
      },
      "embedders": {
        "my_embedder": {"type": "precomputed"}
      },
      "fields": {
        "title": {"text": {"indexed": true, "stored": true, "term_vectors": false, "analyzer": "standard"}},
        "body": {"text": {"indexed": true, "stored": true, "term_vectors": false, "analyzer": "body_analyzer"}},
        "category": {"text": {"indexed": true, "stored": true, "term_vectors": false, "analyzer": "keyword"}},
        "embedding": {"hnsw": {"dimension": 4, "distance": "DISTANCE_METRIC_COSINE", "m": 16, "ef_construction": 200, "embedder": "my_embedder"}}
      },
      "default_fields": ["title", "body"]
    }
  }'

This creates an index with three text fields and one vector field:

  • title — uses the built-in standard analyzer (tokenizes and lowercases).
  • body — uses the custom body_analyzer defined in the analyzers section (NFKC normalization + regex tokenizer + lowercase + custom stop words).
  • category — uses the keyword analyzer (treats the entire value as a single token for exact matching).
  • embedding — HNSW vector index with 4 dimensions, cosine distance, using the my_embedder embedder defined in embedders. In this tutorial we use precomputed (vectors supplied externally). In production, use a dimension matching your embedding model (e.g. 384 or 768).

The default_fields setting means that queries without a field prefix will search both title and body.

Built-in analyzers

standard, keyword, english, japanese, simple, noop. If omitted, the engine default (standard) is used.

Custom analyzer components

You can compose custom analyzers from the following components:

  • Tokenizers: whitespace, unicode_word, regex, ngram, lindera, whole
  • Char filters: unicode_normalization, pattern_replace, mapping, japanese_iteration_mark
  • Token filters: lowercase, stop, stem, boost, limit, strip, remove_empty, flatten_graph

Embedders

The embedders section defines how vectors are generated. Each vector field can reference an embedder by name via the embedder option. Available types:

  • precomputed — vectors are supplied externally (no automatic embedding).
  • candle_bert — local BERT model via Candle. Params: model (HuggingFace model ID). Requires embeddings-candle feature.
  • candle_clip — local CLIP multimodal model. Params: model (HuggingFace model ID). Requires embeddings-multimodal feature.
  • openai — OpenAI API. Params: model (e.g. "text-embedding-3-small"). Requires embeddings-openai feature and OPENAI_API_KEY env var.

Example with a BERT embedder (requires the embeddings-candle feature):

{
  "embedders": {
    "bert": {"type": "candle_bert", "model": "sentence-transformers/all-MiniLM-L6-v2"}
  },
  "fields": {
    "embedding": {"hnsw": {"dimension": 384, "embedder": "bert"}}
  }
}

Verify the index was created:

curl http://localhost:8080/v1/index

Expected response:

{"document_count":0,"vector_fields":{}}

Step 3: Add Documents

Add a few documents to the index. Use PUT to upsert documents by ID. Each document includes text fields and an embedding vector (in production, these vectors would come from an embedding model):

curl -X PUT http://localhost:8080/v1/documents/doc001 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Introduction to Rust Programming",
        "body": "Rust is a modern systems programming language that focuses on safety, speed, and concurrency.",
        "category": "programming",
        "embedding": [0.9, 0.1, 0.2, 0.0]
      }
    }
  }'
curl -X PUT http://localhost:8080/v1/documents/doc002 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Web Development with Rust",
        "body": "Building web applications with Rust has become increasingly popular. Frameworks like Actix and Rocket make it easy to create fast and secure web services.",
        "category": "web-development",
        "embedding": [0.7, 0.3, 0.5, 0.1]
      }
    }
  }'
curl -X PUT http://localhost:8080/v1/documents/doc003 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Python for Data Science",
        "body": "Python is the most popular language for data science and machine learning. Libraries like NumPy and Pandas provide powerful tools for data analysis.",
        "category": "data-science",
        "embedding": [0.1, 0.8, 0.1, 0.9]
      }
    }
  }'

Vector fields are specified as JSON arrays of numbers. The array length must match the dimension configured in the schema (4 in this tutorial).

Step 4: Commit Changes

Documents are not searchable until committed. Commit the pending changes:

curl -X POST http://localhost:8080/v1/commit

Step 5: Search Documents

Search for documents containing “rust”:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "rust", "limit": 10}'

This searches the default fields (title and body). Expected result: doc001 and doc002 are returned.

Search only in the title field:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "title:python", "limit": 10}'

Expected result: only doc003 is returned.

Search by Category

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "category:programming", "limit": 10}'

Expected result: only doc001 is returned.

Boolean Queries

Combine conditions with AND, OR, and NOT:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "rust AND web", "limit": 10}'

Expected result: only doc002 is returned (contains both “rust” and “web”).

Field Boosting

Boost the title field to prioritize title matches:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "rust",
    "limit": 10,
    "field_boosts": {"title": 2.0}
  }'

Search by vector similarity. Provide a query vector in query_vectors and specify which field to search:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query_vectors": [
      {
        "vector": [0.85, 0.15, 0.2, 0.05],
        "fields": ["embedding"]
      }
    ],
    "limit": 10
  }'

This finds documents whose embedding vectors are closest to the query vector. Expected result: doc001 ranks highest (most similar vector).

Combine lexical search and vector search for best results. The fusion parameter controls how scores from both searches are merged:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "rust",
    "query_vectors": [
      {
        "vector": [0.85, 0.15, 0.2, 0.05],
        "fields": ["embedding"]
      }
    ],
    "fusion": {"rrf": {"k": 60.0}},
    "limit": 10
  }'

This uses Reciprocal Rank Fusion (RRF) to merge lexical and vector search results. You can also use weighted sum fusion:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "programming",
    "query_vectors": [
      {
        "vector": [0.85, 0.15, 0.2, 0.05],
        "fields": ["embedding"]
      }
    ],
    "fusion": {"weighted_sum": {"lexical_weight": 0.3, "vector_weight": 0.7}},
    "limit": 10
  }'

Step 6: Retrieve a Document

Fetch a specific document by its ID:

curl http://localhost:8080/v1/documents/doc001

Expected response (includes the stored vector field):

{
  "documents": [
    {
      "fields": {
        "title": "Introduction to Rust Programming",
        "body": "Rust is a modern systems programming language that focuses on safety, speed, and concurrency.",
        "category": "programming",
        "embedding": [0.9, 0.1, 0.2, 0.0]
      }
    }
  ]
}

Step 7: Update a Document

Update a document by PUT-ing with the same ID. This replaces the entire document:

curl -X PUT http://localhost:8080/v1/documents/doc001 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Introduction to Rust Programming",
        "body": "Rust is a modern systems programming language that focuses on safety, speed, and concurrency. It provides memory safety without garbage collection.",
        "category": "programming",
        "embedding": [0.9, 0.1, 0.2, 0.0]
      }
    }
  }'

Commit and verify:

curl -X POST http://localhost:8080/v1/commit
curl http://localhost:8080/v1/documents/doc001

The updated body text is now stored.

Step 8: Delete a Document

Delete a document by its ID:

curl -X DELETE http://localhost:8080/v1/documents/doc003

Commit and verify:

curl -X POST http://localhost:8080/v1/commit

Confirm the document was deleted:

curl http://localhost:8080/v1/documents/doc003

Expected response:

{"documents":[]}

Search results will no longer include the deleted document:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "python", "limit": 10}'

Expected result: no results returned.

Step 9: Check Index Statistics

View the current index statistics:

curl http://localhost:8080/v1/index

The document_count should reflect the remaining documents after the deletion.

Step 10: Clean Up

Stop the server with Ctrl+C. The server performs a graceful shutdown, committing any pending changes before exiting.

To remove the tutorial data:

rm -rf /tmp/laurus/tutorial

Going Further: Using a Real Embedding Model

The tutorial above uses precomputed vectors for simplicity. In production, you typically use an embedding model to automatically convert text into vectors. Here is how to set up a BERT-based embedder.

Prerequisites

Build laurus with the embeddings-candle feature:

cargo build --release --features embeddings-candle

Schema with BERT Embedder

Create an index:

curl -X POST http://localhost:8080/v1/index \
  -H 'Content-Type: application/json' \
  -d '{
    "schema": {
      "embedders": {
        "bert": {
          "type": "candle_bert",
          "model": "sentence-transformers/all-MiniLM-L6-v2"
        }
      },
      "fields": {
        "title": {"text": {"indexed": true, "stored": true, "analyzer": "standard"}},
        "body": {"text": {"indexed": true, "stored": true, "analyzer": "standard"}},
        "embedding": {"hnsw": {"dimension": 384, "distance": "DISTANCE_METRIC_COSINE", "m": 16, "ef_construction": 200, "embedder": "bert"}}
      },
      "default_fields": ["title", "body"]
    }
  }'

The model is automatically downloaded from HuggingFace Hub on first use. The dimension (384) must match the model’s output dimension.

Add documents. Pass text to the embedding field — the embedder automatically converts it to a vector:

curl -X PUT http://localhost:8080/v1/documents/doc001 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Introduction to Rust Programming",
        "body": "Rust is a modern systems programming language.",
        "embedding": "Rust is a modern systems programming language."
      }
    }
  }'
curl -X PUT http://localhost:8080/v1/documents/doc002 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Web Development with Rust",
        "body": "Building web applications with Rust using Actix and Rocket.",
        "embedding": "Building web applications with Rust using Actix and Rocket."
      }
    }
  }'

Commit:

curl -X POST http://localhost:8080/v1/commit

Search with both lexical and semantic queries. The embedder also handles text-to-vector conversion at search time:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "systems programming",
    "query_vectors": [
      {
        "vector": "systems programming language",
        "fields": ["embedding"]
      }
    ],
    "fusion": {"rrf": {"k": 60.0}},
    "limit": 10
  }'

With the precomputed embedder you must pass raw vectors, but text-capable embedders like candle_bert accept text directly for both indexing and searching.

Using OpenAI Embeddings

For OpenAI’s embedding API, set the OPENAI_API_KEY environment variable and build with the embeddings-openai feature:

cargo build --release --features embeddings-openai
export OPENAI_API_KEY="sk-..."

Create an index:

curl -X POST http://localhost:8080/v1/index \
  -H 'Content-Type: application/json' \
  -d '{
    "schema": {
      "embedders": {
        "openai": {
          "type": "openai",
          "model": "text-embedding-3-small"
        }
      },
      "fields": {
        "title": {"text": {"indexed": true, "stored": true}},
        "embedding": {"hnsw": {"dimension": 1536, "distance": "DISTANCE_METRIC_COSINE", "embedder": "openai"}}
      },
      "default_fields": ["title"]
    }
  }'

The text-embedding-3-small model outputs 1536-dimensional vectors.

Available Embedding Models

TypeFeature FlagExample ModelDimension
candle_bertembeddings-candlesentence-transformers/all-MiniLM-L6-v2384
candle_clipembeddings-multimodalopenai/clip-vit-base-patch32512
openaiembeddings-openaitext-embedding-3-small1536

Next Steps