Everything you need to go from zero to running semantic search in your own Val Town project.
A semantic search engine that understands meaning. Unlike normal search (which matches exact words), SlimArmor finds results based on what text means.
Real example:
- You store:
"The patient requires immediate surgery" - You search:
"medical emergency" - It finds it — even though none of the words match ✅
This works because text is converted into embeddings — lists of numbers that capture meaning. Similar meanings produce similar numbers, so we can measure "how close" two pieces of text are in meaning-space.
- Go to val.town/x/kamenxrider/slimarmor
- Click Fork (top right)
- Your own copy is now live!
SlimArmor needs an AI API to convert text into embeddings. The default provider is Nebius — it's free to start and has great quality.
- Sign up at nebius.com
- Go to your API Keys page and create a new key
- Copy the key — you'll need it in the next step
Prefer OpenAI? Set
EMBEDDING_PROVIDER=openaiand use yourOPENAI_API_KEYinstead. See README.md for all provider options.
In your forked val on Val Town:
- Click Settings (the gear icon)
- Go to Environment Variables
- Add these:
| Key | Value | Required? |
|---|---|---|
NEBIUS_API_KEY | Your Nebius API key | ✅ Yes |
ADMIN_TOKEN | Any secret string (e.g. my-secret-123) | Recommended |
ADMIN_TOKEN protects your write endpoints. Without it, anyone can add or delete your data.
Click on api.ts in your val. At the top you'll see an endpoint URL like:
https://yourusername--abc123.web.val.run
Open that URL in a browser — you should see the API info page. That's your SlimArmor instance! 🎉
Visit https://YOUR_ENDPOINT/ui for a terminal-style interface.
Type help to see all commands. To add your first record:
auth your-admin-token
upsert my-first-note "The quick brown fox jumps over the lazy dog"
Then search:
search "animals jumping"
# Replace with your actual values ENDPOINT="https://YOUR_ENDPOINT" TOKEN="your-admin-token" # Add a record curl -X POST $ENDPOINT/upsert \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"id": "note-1", "text": "The quick brown fox jumps over the lazy dog"}' # Search curl -X POST $ENDPOINT/search \ -H "Content-Type: application/json" \ -d '{"query": "animals jumping", "k": 5}'
const ENDPOINT = "https://YOUR_ENDPOINT";
const TOKEN = "your-admin-token";
// Add a record
await fetch(`${ENDPOINT}/upsert`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${TOKEN}`,
},
body: JSON.stringify({
id: "note-1",
text: "The quick brown fox jumps over the lazy dog",
meta: { category: "example" },
}),
});
// Search
const res = await fetch(`${ENDPOINT}/search`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "animals jumping", k: 5 }),
});
const { results } = await res.json();
console.log(results);
A search result looks like this:
{ "id": "note-1", "text": "The quick brown fox jumps over the lazy dog", "meta": { "category": "example" }, "distance": 0.52 }
The key field is distance — it tells you how similar the result is to your query:
| Distance | What it means | Should you include it? |
|---|---|---|
| 0.0 – 0.3 | Near-identical meaning | Always ✅ |
| 0.3 – 0.5 | Very similar | Yes ✅ |
| 0.5 – 0.65 | Related | Usually ✅ |
| 0.65 – 0.75 | Loosely related | Maybe ⚠️ |
| 0.75+ | Probably unrelated | No ❌ |
Use maxDistance to filter out weak matches:
{ "query": "animals jumping", "k": 10, "maxDistance": 0.65 }
Not sure what threshold to use? Use the calibrate endpoint:
GET /calibrate?q=your+search+query
It analyzes your actual data and recommends tight/balanced/loose thresholds.
// Store notes
await fetch(`${ENDPOINT}/upsert`, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${TOKEN}` },
body: JSON.stringify([
{ id: "note-2024-01", text: "Meeting with Sarah about Q4 budget planning", meta: { date: "2024-01", tag: "work" } },
{ id: "note-2024-02", text: "Research best frameworks for mobile development", meta: { date: "2024-01", tag: "tech" } },
{ id: "note-2024-03", text: "Book flight to Amsterdam for the conference", meta: { date: "2024-01", tag: "travel" } },
]),
});
// Find work-related notes
const res = await fetch(`${ENDPOINT}/search`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "work meetings finance", k: 5, maxDistance: 0.65 }),
});
For long documents, use chunked upsert to split them first:
curl -X POST $ENDPOINT/upsert_chunked \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{ "id": "my-essay", "text": "...a very long piece of text...", "meta": { "source": "blog", "author": "Alice" }, "chunkSize": 800, "overlap": 100 }'
Each chunk is stored separately (my-essay::chunk1, my-essay::chunk2, etc.) and can be searched individually.
Use meta fields to organize data, then filter on search:
# Store records with categories curl -X POST $ENDPOINT/upsert \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '[ {"id": "r1", "text": "Python tutorial for beginners", "meta": {"type": "article"}}, {"id": "r2", "text": "Python course on Coursera", "meta": {"type": "course"}}, {"id": "r3", "text": "JavaScript for web development", "meta": {"type": "article"}} ]' # Search only within articles curl -X POST $ENDPOINT/search \ -H "Content-Type: application/json" \ -d '{"query": "learn programming", "k": 5, "filters": {"type": "article"}}'
// Store FAQ pairs using the question as the text
await fetch(`${ENDPOINT}/upsert`, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${TOKEN}` },
body: JSON.stringify([
{ id: "faq-1", text: "How do I reset my password?", meta: { answer: "Go to Settings → Security → Reset Password" } },
{ id: "faq-2", text: "How do I cancel my subscription?", meta: { answer: "Go to Billing → Cancel Plan" } },
{ id: "faq-3", text: "How do I contact support?", meta: { answer: "Email support@example.com" } },
]),
});
// When a user asks a question, find the closest FAQ
const userQuestion = "I forgot my password, what do I do?";
const res = await fetch(`${ENDPOINT}/search`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: userQuestion, k: 1, maxDistance: 0.6 }),
});
const { results } = await res.json();
if (results.length > 0) {
console.log("Answer:", results[0].meta.answer);
}
Instead of using the HTTP API, you can import SlimArmor's core directly into another val:
import * as db from "https://esm.town/v/kamenxrider/slimarmor/vectordb.ts";
export default async function handler(req: Request) {
// Setup runs once per cold start (idempotent)
await db.setup();
const url = new URL(req.url);
if (req.method === "POST" && url.pathname === "/add") {
const { id, text } = await req.json();
await db.upsert(id, text);
return Response.json({ ok: true });
}
if (req.method === "POST" && url.pathname === "/find") {
const { query } = await req.json();
const results = await db.search(query, 5, 0.65);
return Response.json({ results });
}
return new Response("Not found", { status: 404 });
}
The library uses your val's own SQLite database — you don't need to run the API separately. Just import and use.
- Use meaningful IDs —
blog-post-2024-01is better than1 - Keep text focused — shorter, topic-focused chunks search better than walls of text
- Use metadata — store category, date, author, tags etc. so you can filter later
- Calibrate your threshold — use
/calibrate?q=...before going to production - Batch your upserts — send arrays of records instead of one at a time (much faster)
- Storing empty or near-duplicate text — SlimArmor deduplicates by content hash, so identical text won't re-embed, but similar-but-different text will generate redundant embeddings
- Deleting via raw SQL — always use
POST /clearorPOST /deleteso the vector index stays in sync - Switching providers without clearing — embeddings from different models are completely incompatible
- Deduplication is automatic — if you upsert the same
idwith the same text, it skips the embedding API call and only updates metadata. You can safely re-run upserts. - Hybrid search helps with specific terms — if your data has product codes, names, or exact terms, enable
hybrid: { enabled: true }to boost keyword matches. /validateis your friend — run it after setup to confirm everything is working before adding real data.
Make sure you're sending the header: Authorization: Bearer YOUR_ADMIN_TOKEN
In the browser CLI, type auth your-token first.
Your API key is wrong or expired. Go to your val's Settings → Environment Variables and update NEBIUS_API_KEY (or whichever provider you're using).
- Run
/calibrate?q=your+queryto see distance distributions - Try lowering
maxDistance - Try enabling hybrid search:
"hybrid": {"enabled": true}
The DiskANN index got out of sync (happens if you manually deleted rows via SQL). Fix it with:
curl -X POST $ENDPOINT/reindex -H "Authorization: Bearer $TOKEN"
Normal — each batch of records requires one API call to the embedding provider (~460ms). For bulk imports, batch as many records as possible in each /upsert call (arrays of up to ~96 records per batch are ideal).
ENDPOINT="https://YOUR_ENDPOINT" TOKEN="your-admin-token" # Health check curl $ENDPOINT/ping # View stats curl $ENDPOINT/stats # Add one record curl -X POST $ENDPOINT/upsert -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \ -d '{"id":"doc-1","text":"Your text here","meta":{"category":"notes"}}' # Add many records curl -X POST $ENDPOINT/upsert -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \ -d '[{"id":"a","text":"first"},{"id":"b","text":"second"}]' # Search curl -X POST $ENDPOINT/search -H "Content-Type: application/json" \ -d '{"query":"your query","k":10,"maxDistance":0.65}' # Search with filter curl -X POST $ENDPOINT/search -H "Content-Type: application/json" \ -d '{"query":"your query","k":10,"filters":{"category":"notes"}}' # Get a record curl "$ENDPOINT/get?id=doc-1" # List IDs curl "$ENDPOINT/list?limit=20" # Delete a record curl -X POST $ENDPOINT/delete -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \ -d '{"id":"doc-1"}' # Calibrate threshold curl "$ENDPOINT/calibrate?q=your+query" # Seed test data curl -H "Authorization: Bearer $TOKEN" "$ENDPOINT/seed?n=50" # Export curl -H "Authorization: Bearer $TOKEN" "$ENDPOINT/export?limit=500" # Clear all (careful!) curl -X POST "$ENDPOINT/clear?confirm=yes" -H "Authorization: Bearer $TOKEN" # Rebuild index curl -X POST $ENDPOINT/reindex -H "Authorization: Bearer $TOKEN"
Happy searching! 🔍 If you get stuck, open the /ui browser CLI and type help.