Get started

Introduction

DNA is a search engine that learns from your feedback.

You send embeddings of your items and your queries. DNA returns the best item for each query, you score it, and the next search is better. DNA never sees your data, only vectors, keys and scores.

Quick start

  1. Get a key

    Write to office@hashgrid.ai. Send the key with every request to https://dna.hashgrid.ai/api/v1.

    Authorization: Bearer hg-…
  2. Add your items

    Embed each item with any model, and give it a key under one prefix, like /items/. Keep the ID you get back.

    POST /nodes
    { "key": "/items/42", "emb": [0.012, -0.045, …] }
    
    ← { "id": "f43125171437b656", … }
  3. Add a query

    Embed what you are looking for with the same model. No key: a query searches, it isn't searched for.

    POST /nodes
    { "emb": [0.031, 0.008, …] }
    
    ← { "id": "93d77a9cf9dcdcbb", … }
  4. Search

    Ask for a result under your items' prefix. You get the best item DNA has not shown this query yet.

    POST /nodes/93d77a9cf9dcdcbb/edges
    { "prefix": "/items/" }
    
    ← {
      "id": "f43125171437b656",
      "score": null
    }
  5. Score it

    Tell DNA how good the result was, from -1 to 1.

    PATCH /nodes/93d77a9cf9dcdcbb/edges/f43125171437b656
    { "score": 0.72 }

Anonymity

DNA is anonymous by design: it only ever receives vectors, random IDs, the keys you choose, and scores, never your data, your prompts or your users. That split is deliberate. Keys are stored as you send them, so use opaque ones, like /items/42, if names are sensitive. Three things stay with you, and you manage them:

  • The embedder: the model that turns your items and queries into vectors.
  • The judge: whatever scores results, be it an LLM, a smaller model, or your users.
  • The collection: your items themselves, and which ID or key belongs to which item.

Connect an agent (MCP)

DNA is also an MCP server, at https://dna.hashgrid.ai/mcp, so agents can use it as a tool. It takes the same API key (no key yet? write to us), and like the API it takes embeddings, not text. In Claude Code:

claude mcp add --transport http dna https://dna.hashgrid.ai/mcp \
  --header "Authorization: Bearer hg-…"

In Cursor and other clients configured with JSON:

{
  "mcpServers": {
    "dna": {
      "url": "https://dna.hashgrid.ai/mcp",
      "headers": { "Authorization": "Bearer hg-…" }
    }
  }
}

There is one tool per API route, named after it, taking the same fields; create_edge is the search:

  • get_grid: GET /
  • list_nodes, get_node, create_node, update_node, delete_node: the node routes
  • list_edges, get_edge, create_edge, put_edge, update_edge, delete_edge: the edge routes

Concepts

Grids

A grid is your space: a collection of items and queries, and your scores. Each grid has exactly one API key, and each key opens exactly one grid.

Nodes

A node is anything DNA can search for or search with. It holds:

  • key (optional): where the node lives, a path like /items/42, unique in the grid. Searches look under a prefix of it, like /items/.
  • emb: its embedding, what the node is, from any embedding model. Use one model per grid.

Creating a node

POST /nodes

Send its embedding, emb, 1 to 2048 numbers, and for an item a key: a path of up to 1024 characters, unique in the grid. A key that is already taken gives a 409.

POST /nodes
{ "key": "/items/42", "emb": [0.012, -0.045, …] }

← 201 {
  "id": "f43125171437b656",
  "key": "/items/42",
  "emb": [0.012, -0.045, …]
}

Getting a node

GET /nodes/{nid}

Returns its key, null if it has none, and its embedding.

GET /nodes/f43125171437b656

← 200 {
  "id": "f43125171437b656",
  "key": "/items/42",
  "emb": [0.012, -0.045, …]
}

Listing nodes

GET /nodes

Up to limit nodes (32 by default, at most 1024). For the next page, pass the last ID as cursor.

GET /nodes?limit=100&cursor=f43125171437b656

← 200 [{ "id": "…", "key": "…", "emb": […] }, …]

Updating a node

PATCH /nodes/{nid}

Replace the embedding, move the node to another key, or send "key": null so it can no longer be found. Fields you leave out stay as they are. Its scores are kept, and keep training on the new embedding.

PATCH /nodes/f43125171437b656
{ "key": "/archive/items/42" }

← 200 {
  "id": "f43125171437b656",
  "key": "/archive/items/42",
  "emb": [0.012, -0.045, …]
}

Deleting a node

DELETE /nodes/{nid}

Deletes the node and its edges, both ways: it no longer searches or shows up in results, and its scores no longer train DNA.

DELETE /nodes/f43125171437b656

← 204

Edges

An edge is a result for a query: a pair of nodes and a score from -1 (wrong) to 1 (perfect), or null until scored. Searching creates edges, and scoring them is how DNA learns. Scores can come from an LLM, a smaller model, or your users.

Scoring a result

PATCH /nodes/{nid}/edges/{eid}

Set your score, from -1 to 1. Only scores train DNA: a result you never score doesn't.

PATCH /nodes/93d77a9cf9dcdcbb/edges/f43125171437b656
{ "score": 0.72 }

← 200 {
  "id": "f43125171437b656",
  "score": 0.72
}

Scoring any pair

PUT /nodes/{nid}/edges/{eid}

Already know how well an item fits a query? Score the pair directly, no search needed.

PUT /nodes/93d77a9cf9dcdcbb/edges/2b1f0c9e4d7a8f31
{ "score": 1 }

← 201 {
  "id": "2b1f0c9e4d7a8f31",
  "score": 1.0
}

Getting an edge

GET /nodes/{nid}/edges/{eid}

One result and its score, null if it has none yet.

GET /nodes/93d77a9cf9dcdcbb/edges/f43125171437b656

← 200 {
  "id": "f43125171437b656",
  "score": 0.72
}

Listing edges

GET /nodes/{nid}/edges

A query's results so far, paged like nodes, with the last id as cursor.

GET /nodes/93d77a9cf9dcdcbb/edges?limit=100

← 200 [{ "id": "…", "score": 0.72 }, …]

Deleting an edge

DELETE /nodes/{nid}/edges/{eid}

Forgets the pair, so it can come up again. Its score no longer trains DNA.

DELETE /nodes/93d77a9cf9dcdcbb/edges/f43125171437b656

← 204

Guides

Warming up your grid

A new grid knows only your embeddings, so its first results are rough. A warm-up teaches it your data before real searches arrive.

  1. Add all your items first. DNA can only return what it has.
  2. Give it scores: from a dataset you already have, or by searching and scoring warm-up queries.

If you have an existing dataset

Already know how well some items fit some queries, from labels or past clicks? Score those pairs directly, no search needed. They train DNA like any other score.

# Scores you already have, from labels or past clicks.
for query_id, item_id, score in dataset:
    dna.put(f"/nodes/{query_id}/edges/{item_id}", json={"score": score})

Keep scoring after the warm-up, less and less. See when to stop scoring.

When to stop scoring

As DNA learns, its order gets closer to what your judge would say, and you can skip the judge more often. That is where the savings come from. A simple way to decide: score only the first few results of each search, say 3, and trust DNA's order for the rest.

for rank in range(10):
    r = dna.post(f"/nodes/{query_id}/edges", json={"prefix": "/items/"})
    if r.status_code == 400:  # nothing left for this query
        break
    item_id = r.json()["id"]
    if rank < 3:  # the first few: ask your judge
        score = judge(queries[query_id], items[item_id])
        dna.patch(f"/nodes/{query_id}/edges/{item_id}", json={"score": score})
    use(items[item_id])  # the rest: trust DNA's order

Start strict and loosen as DNA's order proves right. The numbers above are only a starting point.