Eloquent

Documentation

Knowledge Graph

The Knowledge Graph service provides semantic search and graph query capabilities. Use this for ingesting nodes, querying relationships, building product catalogs, or implementing semantic search.

Overview

The Knowledge Graph stores structured data with relationships and vector embeddings, enabling:

  • Semantic Search: Find similar items using vector similarity
  • Graph Queries: Traverse relationships between nodes
  • Product Catalogs: Build searchable catalogs with rich relationships

Core Concepts

KG Node

A node in the knowledge graph:

interface KGNode {
  id: string;
  graphId: string;
  label: string;           // Node type (product, category, user)
  name: string;            // Display name
  description?: string;
  properties: Record<string, any>;
  embedding?: number[];    // Vector embedding for semantic search
  createdAt: string;
  updatedAt: string;
}

KG Edge

A relationship between nodes:

interface KGEdge {
  id: string;
  graphId: string;
  sourceId: string;
  targetId: string;
  label: string;           // Relationship type (belongs_to, related_to)
  properties?: Record<string, any>;
}

API Endpoints

MethodPathDescription
POST/api/v1/kg/ingest/nodesIngest nodes
POST/api/v1/kg/ingest/edgesIngest edges
POST/api/v1/kg/query/searchSemantic search
POST/api/v1/kg/query/traverseGraph traversal
GET/api/v1/kg/graphsList graphs
GET/api/v1/kg/graphs/{id}/labelsGet labels in graph

Semantic Search

Search Request

interface SearchRequest {
  graphId: string;
  query: string;           // Natural language query
  labels?: string[];       // Filter by node labels
  limit?: number;          // Max results (default: 10)
  threshold?: number;      // Similarity threshold (0-1)
}

Frontend Usage

import { useKnowledgeGraph } from "@/hooks/use-knowledge-graph";

function ProductSearch() {
  const { search, results, loading } = useKnowledgeGraph();

  const handleSearch = async (query: string) => {
    await search({
      graphId: "products-graph",
      query,
      labels: ["product"],
      limit: 20,
    });
  };

  return (
    <div>
      <input
        type="text"
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="Search products..."
      />
      {loading && <Spinner />}
      {results.map((node) => (
        <ProductCard key={node.id} product={node} />
      ))}
    </div>
  );
}

Ingesting Data

Ingest Nodes

const nodes: KGNodeInput[] = [
  {
    label: "product",
    name: "iPhone 15",
    description: "Latest Apple smartphone",
    properties: {
      price: 999,
      category: "electronics",
      brand: "Apple",
    },
  },
  {
    label: "category",
    name: "Electronics",
    properties: {
      displayOrder: 1,
    },
  },
];

await ingestNodes({
  graphId: "products-graph",
  nodes,
});

Ingest Edges

const edges: KGEdgeInput[] = [
  {
    sourceId: "product-123",
    targetId: "category-456",
    label: "belongs_to",
  },
  {
    sourceId: "product-123",
    targetId: "product-789",
    label: "related_to",
    properties: {
      similarity: 0.85,
    },
  },
];

await ingestEdges({
  graphId: "products-graph",
  edges,
});

Graph Traversal

Traverse Request

interface TraverseRequest {
  graphId: string;
  startNodeId: string;
  direction: "outbound" | "inbound" | "any";
  edgeLabels?: string[];
  depth?: number;
  limit?: number;
}

Example: Find Related Products

const related = await traverse({
  graphId: "products-graph",
  startNodeId: "product-123",
  direction: "outbound",
  edgeLabels: ["related_to"],
  depth: 2,
  limit: 10,
});

Backend Implementation

Search Handler

func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
    orgID := r.Header.Get("X-Org-ID")

    var req SearchRequest
    json.NewDecoder(r.Body).Decode(&req)

    // Generate embedding for query
    embedding, err := h.embedder.Embed(r.Context(), req.Query)
    if err != nil {
        writeError(w, http.StatusInternalServerError, "EMBED_FAILED", err.Error())
        return
    }

    // Search by vector similarity
    results, err := h.service.VectorSearch(
        r.Context(),
        orgID,
        req.GraphID,
        embedding,
        req.Labels,
        req.Limit,
        req.Threshold,
    )
    if err != nil {
        writeError(w, http.StatusInternalServerError, "SEARCH_FAILED", err.Error())
        return
    }

    writeJSON(w, http.StatusOK, SearchResponse{Nodes: results})
}

ClickHouse Storage

Knowledge graph data is stored in ClickHouse with per-org isolation:

-- Nodes table
CREATE TABLE org_{orgId}.kg_nodes (
    id String,
    graph_id String,
    label String,
    name String,
    description String,
    properties String,  -- JSON
    embedding Array(Float32),
    created_at DateTime64,
    updated_at DateTime64
) ENGINE = MergeTree()
ORDER BY (graph_id, label, id);

-- Vector search index
ALTER TABLE org_{orgId}.kg_nodes
ADD INDEX embedding_idx embedding TYPE annoy('L2Distance', 100);

Best Practices

  1. Use meaningful labels - Clear node/edge types improve queries
  2. Generate embeddings on ingest - Pre-compute for search performance
  3. Batch ingest operations - Ingest multiple nodes/edges at once
  4. Set appropriate thresholds - Balance precision vs recall
  5. Index frequently queried properties - Configure in ClickHouse

Next Steps