AntflyAntfly

Build a Knowledge Graph from Your Docs

A table that extracts entities and relations from each document, resolves them into one entity table, and answers search and graph queries over both

The Result#

Each document you write is chunked and embedded for search, and GLiNER extracts the components it mentions and how they relate. Mentions of the same name in different documents resolve to one entity, so one query reaches from an entity to every related entity and section:

{"key": "entity/backup_service", "table": "entities", "depth": 1,
 "document": {"canonical_name": "backup service", "entity_type": "component"},
 "path_edges": [{"type": "depends_on", "weight": 0.985,
   "from": {"key": "entity/storage_engine", "table": "entities"},
   "to": {"key": "entity/backup_service", "table": "entities"}, "direction": "in"}]}

Before You Start#

The embedding and extraction models pulled:

antfly inference pull hf:google/embeddinggemma-2:bf16-safetensors-bundle-v1
antfly inference pull fastino/gliner2.5-base-v1:safetensors@54785d51df8d86a0d8f2eb212fec15871aad2c4d --tasks extract
antfly inference list

antfly inference list shows one embedder and one extractor line. Then, in another terminal, Antfly running in standalone mode:

antfly standalone

Build It#

1. Create the Entity Table#

The resolver promotes each canonical entity into this table, so it must exist first:

curl -X POST http://127.0.0.1:8080/db/v1/tables/entities \
  -H 'Content-Type: application/json' -d '{"num_shards": 1}'

2. Create the Docs Table#

chunk_vectors chunks and embeds each body for semantic search. knowledge is a graph index: an extractor produces entities and relations per document, source turns each relation into an edge, and the resolver gives every mention a key such as entity/storage_engine, adding a mentions edge from the document to it.

curl -X POST http://127.0.0.1:8080/db/v1/tables/docs \
  -H 'Content-Type: application/json' -d '{
  "num_shards": 1,
  "indexes": {
    "chunk_vectors": {
      "type": "embeddings",
      "sources": [{"artifact": "chunk_dense"}],
      "embedder": {"provider": "antfly", "model": "google/embeddinggemma-2"},
      "distance_metric": "cosine",
      "enrichments": [
        {"name": "chunks", "kind": "chunk", "field": "body",
         "chunker_json": "{\"provider\":\"antfly\",\"model\":\"fixed\",\"text\":{\"target_tokens\":400,\"overlap_tokens\":40}}"},
        {"name": "chunk_dense", "kind": "embedding", "field": "body",
         "source_artifact_name": "chunks", "expected_dims": 768}
      ]
    },
    "knowledge": {
      "type": "graph",
      "artifact": {
        "name": "relations", "kind": "asset", "content_type": "application/json",
        "source": {"type": "field", "value": "body"},
        "producer_json": {"type": "extractor", "config": {
          "provider": "antfly", "model": "fastino/gliner2.5-base-v1",
          "schema": {
            "entities": ["component", "subsystem", "protocol", "model", "format"],
            "relations": [{"type": "depends_on"}, {"type": "implements"}, {"type": "owns"}]
          },
          "options": {"include_confidence": true, "include_spans": true}
        }}
      },
      "source": {
        "artifact": "relations", "path": "$.relations[*]",
        "format": "extraction_relation", "mention_edge_type": "mentions",
        "edge": {"type": "{{ _item.type }}", "weight": "{{ _item.score }}"}
      },
      "edge_types": [{"name": "mentions"}, {"name": "depends_on"}, {"name": "implements"}, {"name": "owns"}],
      "resolvers": [{
        "name": "entities", "table": "entities",
        "source_artifact": "relations", "resolution_artifact": "entity_resolution",
        "key_template": "entity/{{ slug _entity.text }}",
        "min_confidence": 0.5, "config_generation": 1
      }]
    }
  }
}'

3. Write Documents#

curl -X POST http://127.0.0.1:8080/db/v1/tables/docs/batch \
  -H 'Content-Type: application/json' -d '{"inserts": {
  "storage": {"title": "Storage engine", "body": "The storage engine depends on the LSM tree for durable writes. Each shard owns one LSM tree and writes segments in the AFB format."},
  "backups": {"title": "Backups", "body": "The backup service depends on the storage engine and writes portable archives in the AFB format. The metadata server owns the backup schedule."},
  "raft": {"title": "Raft consensus", "body": "The metadata server depends on Raft to replicate table definitions. Each shard group implements Raft with its own leader."}
}}'

4. Wait for Enrichment#

antfly index wait --table docs --index chunk_vectors --until complete --timeout 10m
antfly index wait --table docs --index knowledge --until complete --timeout 10m

On an Apple silicon laptop both waits return in under 30 seconds for these three documents. The entities table now holds one document per entity, such as {"canonical_name": "Raft", "entity_type": "protocol"}.

5. Search, Then Walk the Graph#

$query_results seeds a traversal from the hybrid search hits, so one request returns the matching sections and the entities they mention:

curl -X POST http://127.0.0.1:8080/db/v1/tables/docs/query \
  -H 'Content-Type: application/json' -d '{
  "full_text_search": {"match": "storage writes", "field": "body"},
  "semantic_search": "how are writes made durable",
  "indexes": ["chunk_vectors"], "limit": 2, "fields": ["title"],
  "graph_queries": {"related": {"index": "knowledge", "traverse": {
    "start": {"result_ref": "$query_results", "limit": 2},
    "direction": "both", "max_depth": 2, "include_documents": true, "fields": ["title"]
  }}}
}'

The hits are storage and backups, and graph_results.related.nodes lists entity/afb_format, entity/shard, entity/storage_engine, entity/backup_service, and entity/metadata_server.

6. Explore One Entity#

Start from an entity to see what relates to it and which sections mention it. Entities live in another table, so name it in identities:

curl -X POST http://127.0.0.1:8080/db/v1/tables/docs/query \
  -H 'Content-Type: application/json' -d '{
  "limit": 0,
  "graph_queries": {"neighbors": {"index": "knowledge", "traverse": {
    "start": {"identities": [{"key": "entity/storage_engine", "table": "entities"}]},
    "direction": "both", "max_depth": 1, "include_paths": true,
    "include_documents": true, "fields": ["title", "canonical_name", "entity_type"]
  }}}
}'

The result is entity/backup_service over a depends_on edge, plus the storage and backups sections over mentions edges.

Tradeoffs#

The extraction schema decides what the graph can answer. GLiNER only finds the entity labels and relation types you list, so name the things your readers ask about ("component", "protocol") rather than generic labels, and keep relation types few and specific. min_confidence trades recall for precision: below about 0.5, weak mentions mint entities that connect unrelated sections. The resolver keys entities by name alone, so "the storage engine" in two documents becomes one node, but two different things with the same name also merge. The embedder and the extractor share Antfly Inference's memory, so extraction can wait while chunks are embedded; the graph lags the vector index during a large ingest.

Use Agent Skills#

Everything above is also encoded in the Antfly skill, so a coding agent can execute this guide for you:

npx skills add antflydb/antfly-skills

Then prompt it with the outcome, for example "build a knowledge graph of the components in my design docs and show me what depends on the storage engine", and use this page to judge the result.

Next Steps#