<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dan Shalev</title>
    <description>The latest articles on DEV Community by Dan Shalev (@danshalev7).</description>
    <link>https://dev.to/danshalev7</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2251943%2F12ead377-c03a-4292-91db-4efd2bae17ef.png</url>
      <title>DEV Community: Dan Shalev</title>
      <link>https://dev.to/danshalev7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danshalev7"/>
    <language>en</language>
    <item>
      <title>I built a GraphRAG demo with FalkorDB’s new SDK, then benchmarked it against Neo4j</title>
      <dc:creator>Dan Shalev</dc:creator>
      <pubDate>Wed, 29 Apr 2026 04:59:16 +0000</pubDate>
      <link>https://dev.to/danshalev7/i-built-a-graphrag-demo-with-falkordbs-new-sdk-then-benchmarked-it-against-neo4j-3hh</link>
      <guid>https://dev.to/danshalev7/i-built-a-graphrag-demo-with-falkordbs-new-sdk-then-benchmarked-it-against-neo4j-3hh</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk1029x9skgq1hd7uhbe0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk1029x9skgq1hd7uhbe0.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;FalkorDB shipped graphrag-sdk v1.0.0rc1 and I wanted to see how it feels on real content, not a toy dataset. An afternoon of "let me just try it" turned into a few days of "if I'm going to have an opinion, I should measure it against something."&lt;/p&gt;

&lt;p&gt;The something, obviously, was neo4j-graphrag. Same corpus, same LLM, same embedder, same 25-question set, same blind judge. The whole thing — ingest, 25 queries, and the judge rubric across both stacks — costs about $0.15 to reproduce end-to-end.&lt;/p&gt;

&lt;p&gt;This is a write-up of what I did, what broke, what the numbers actually say, and what I'd do differently. I'm not here to crown a winner. I'm here to show what it took to compare them honestly.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="//github.com/FalkorDB/graphrag-sdk-demo"&gt;github.com/FalkorDB/graphrag-sdk-demo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The corpus and the pipeline&lt;/p&gt;

&lt;p&gt;The corpus is 8 FalkorDB blog posts and case studies, roughly 140 KB of Markdown. Topics range from "what is GraphRAG" to the Securin threat-intel case study to a March 2026 cybersecurity webinar announcement. That mix matters later: some questions are short factual lookups, some need multi-hop joining across documents, some ask for specific numbers buried in a single paragraph.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             ┌───────────────────┐
             │  scrape.py        │  Firecrawl → content/*.md
             └─────────┬─────────┘
                       │
            ┌──────────┴──────────┐
            ▼                     ▼
 ┌───────────────────┐   ┌───────────────────┐
 │  ingest.py        │   │  neo4j_ingest.py  │
 │  GraphRAG (async) │   │  SimpleKGPipeline │
 │  + postprocess.py │   │  + entity embeds  │
 └─────────┬─────────┘   └─────────┬─────────┘
           ▼                       ▼
  ┌────────────────┐      ┌────────────────┐
  │  FalkorDB      │      │  Neo4j 5.24    │
  │  :6379 / :3000 │      │  :7474 / :7687 │
  └────────┬───────┘      └───────┬────────┘
           │                      │
           └──────────┬───────────┘
                      ▼
         ┌──────────────────────────┐
         │ benchmark_compare.py     │
         │ bench/ (25 Qs + judge)   │
         │ → COMPARISON.md          │
         └──────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Stack: Python 3.14, graphrag-sdk[litellm]==1.0.0rc1, neo4j-graphrag[openai]&amp;gt;=1.14, LiteLLM, FalkorDB and Neo4j in docker-compose.yml. The LLM is gpt-4o-mini (extraction + generation) at temperature=0. Embedder is text-embedding-3-small (1536 dim). The judge is gpt-4o with temperature=0 and seed=42. Chunking on both sides is fixed-size 1000 with 100 overlap, no approximation. Extraction is open-schema on both sides — no hand-tuned GraphSchema. I wanted the libraries to show their defaults, not my schema design.&lt;/p&gt;

&lt;p&gt;Part 1 — Getting the FalkorDB demo working&lt;br&gt;
The FalkorDB ingest is almost boring to write down, which is the point. The whole thing fits in roughly 60 lines:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;async with GraphRAG(&lt;br&gt;
    connection=ConnectionConfig(host="localhost", graph_name="falkordb_blog_kg"),&lt;br&gt;
    llm=LiteLLM(model="openai/gpt-4o-mini"),&lt;br&gt;
    embedder=LiteLLMEmbedder(model="openai/text-embedding-3-small"),&lt;br&gt;
) as rag:&lt;br&gt;
    for path in content_files:&lt;br&gt;
        text = path.read_text(encoding="utf-8")&lt;br&gt;
        await rag.ingest(path.stem, text=text)&lt;/code&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stats = await rag.finalize()   # dedup + embeddings + indexes`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;ingest() is per-file; finalize() is the cleanup pass that deduplicates entities, backfills embeddings, and creates the HNSW indexes. After the 8 files, I had 509 nodes, 1 228 edges, 160 LLM calls, ~230 seconds wall time, and a cost of $0.054. For an afternoon of "try the SDK," this is a good story.&lt;/p&gt;

&lt;p&gt;Then I opened the FalkorDB browser and it was a hairball.&lt;/p&gt;

&lt;p&gt;Every relationship came out as [:RELATES {rel_type: "USES"}], [:RELATES {rel_type: "INTEGRATES_WITH"}], and so on. The relation type lives as a property on a single generic edge, not as the edge label itself. This is fine for the retriever — it reads the property — but it's ugly in the browser and it's a pain to query by hand (WHERE r.rel_type = 'USES' is not index-accelerated in FalkorDB; the skill docs are explicit about this).&lt;/p&gt;

&lt;p&gt;So I wrote postprocess.py. Two idempotent passes:&lt;/p&gt;

&lt;p&gt;`# Promote (:Entity)-[:RELATES {rel_type:'INTEGRATES_WITH'}]-&amp;gt;(:Entity)&lt;/p&gt;

&lt;h1&gt;
  
  
  into real typed edges.
&lt;/h1&gt;

&lt;p&gt;for raw in distinct_rel_types:&lt;br&gt;
    safe = &lt;em&gt;TYPE_SAFE.sub("", raw.upper().replace(" ", "&lt;/em&gt;").replace("-", "_"))&lt;br&gt;
    graph.query(&lt;br&gt;
        "MATCH (a)-[r:RELATES {rel_type: $t}]-&amp;gt;(b) "&lt;br&gt;
        f"MERGE (a)-[r2:&lt;code&gt;{safe}&lt;/code&gt;]-&amp;gt;(b) "&lt;br&gt;
        "SET r2.fact = r.fact, r2.description = r.description, "&lt;br&gt;
        "    r2.source_chunk_ids = r.source_chunk_ids, r2.spans = r.spans "&lt;br&gt;
        "DELETE r",&lt;br&gt;
        params={"t": raw},&lt;br&gt;
    )`&lt;br&gt;
“&lt;/p&gt;

&lt;p&gt;The string-substitution into the Cypher is necessary because relationship types can't be parameterized — so I sanitize the type name to [A-Z0-9_] before interpolating. The result: 336 generic RELATES became 161 real typed edges (INTEGRATES_WITH, SUPPORTS, USES, ...), idempotently, on every re-ingest.&lt;/p&gt;

&lt;p&gt;The other thing I learned by looking, not by reading docs, is that the SDK does two LLM calls per query: a keyword-extraction pass over the question, then the final generation. Between them the retriever ranks candidate entities deterministically by term frequency — no LLM. I only nailed this down when I ran GRAPH.SLOWLOG against a corrected benchmark harness (my first version was double-counting a call; see the addendum). It matters later in the numbers.&lt;/p&gt;

&lt;p&gt;Part 2 — "Is this actually good?"&lt;/p&gt;

&lt;p&gt;My five demo questions produced answers that looked great. That proved nothing. Cherry-picking five queries against a knowledge graph is not evidence; it is ambience.&lt;/p&gt;

&lt;p&gt;I needed three things: a question set with known ground truth, a comparable second stack so the numbers meant something in context, and a judge that didn't know which stack produced which answer.&lt;/p&gt;

&lt;p&gt;Before writing any benchmark code I wrote down the fairness constraints: same corpus, same chunking (1000/100, fixed-size), same LLM, same embedder, same 25 questions, same judge with a fixed seed, same price sheet for cost math. Anything I couldn't equalize, I would disclose.&lt;/p&gt;

&lt;p&gt;Part 3 — Building the Neo4j side&lt;/p&gt;

&lt;p&gt;This was where it got interesting. neo4j-graphrag is a good library, but the defaults don't give you parity with FalkorDB's out-of-the-box retrieval; you have to build it.&lt;/p&gt;

&lt;p&gt;Four non-obvious things bit me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Document dedup on a stale path. SimpleKGPipeline writes Document.path = 'document.txt' for every file and deduplicates on that path. If you loop over your files, the second file silently merges into the first. The fix is to rename the freshly-created Document node to the source slug right after each run_async().&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Missing chunk vector index. The pipeline writes chunk embeddings as properties, but doesn't always create the vector index to query them. create_vector_index(driver, CHUNK_INDEX, ...) after ingest, manually.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No entity embeddings at all. This one took me a while. FalkorDB builds an entity HNSW as part of finalize(); SimpleKGPipeline does not. So after ingest, I walk every &lt;strong&gt;Entity&lt;/strong&gt;, embed name + description in batches of 64, write with db.create.setNodeVectorProperty, and then create an entity_embedding_idx. Without this pass, "entity vector search" on the Neo4j side would have been meaningless and the comparison would have been dishonest.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GraphRAG rejects custom composite retrievers. This is the fun one. I wanted a retriever that mirrors FalkorDB's MultiPathRetrieval: vector search over entities with 1-hop fact expansion, plus vector search over chunks. In neo4j-graphrag, the obvious shape is two VectorCypherRetrievers composed into a wrapper. But when you pass a wrapper into GraphRAG(...), its pydantic validation rejects anything that isn't a Retriever subclass.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I drove the retrievers directly instead. About 40 lines cleaner:&lt;/p&gt;

&lt;p&gt;`ENTITY_QUERY = """&lt;br&gt;
WITH node, score&lt;br&gt;
OPTIONAL MATCH (node)-[r]-(nbr:&lt;strong&gt;Entity&lt;/strong&gt;)&lt;br&gt;
WITH node, score,&lt;br&gt;
     collect(DISTINCT {&lt;br&gt;
        rel: type(r),&lt;br&gt;
        neighbour: coalesce(nbr.name, nbr.id, ''),&lt;br&gt;
        fact: coalesce(r.fact, r.description, '')&lt;br&gt;
     })[..8] AS facts&lt;br&gt;
RETURN coalesce(node.name, node.id, '') AS entity_name,&lt;br&gt;
       labels(node) AS entity_labels,&lt;br&gt;
       coalesce(node.description, '') AS entity_description,&lt;br&gt;
       facts, score&lt;br&gt;
"""&lt;/p&gt;

&lt;p&gt;self.entity = VectorCypherRetriever(&lt;br&gt;
    driver=driver, index_name="entity_embedding_idx",&lt;br&gt;
    retrieval_query=ENTITY_QUERY, result_formatter=_entity_fmt,&lt;br&gt;
    embedder=embedder, neo4j_database=NEO4J_DATABASE,&lt;br&gt;
)&lt;br&gt;
self.chunk = VectorCypherRetriever(&lt;br&gt;
    driver=driver, index_name="chunk_embedding_idx",&lt;br&gt;
    retrieval_query=CHUNK_QUERY, result_formatter=_chunk_fmt,&lt;br&gt;
    embedder=embedder, neo4j_database=NEO4J_DATABASE,&lt;br&gt;
)`&lt;/p&gt;

&lt;p&gt;Then: build context → prompt → llm.invoke(). Behavior is equivalent to what GraphRAG would do internally; I just don't get the pydantic validator in my way.&lt;/p&gt;

&lt;p&gt;There's a tradeoff worth naming: I did not write a Neo4j equivalent of postprocess.py's typed-edge promotion. I thought about it. I decided that giving Neo4j a cleanup pass that its pipeline doesn't provide would bias toward Neo4j, and the whole point was to compare defaults. So Neo4j keeps its out-of-the-box relation-type distribution (226 types across 785 nodes) while FalkorDB gets the cleanup it natively benefits from (154 types across 504 nodes). I'll come back to this in the asymmetries section.&lt;/p&gt;

&lt;p&gt;Part 4 — Making cost and token tracking honest&lt;/p&gt;

&lt;p&gt;Token counting is the part of a benchmark that you'd think is easy and is not.&lt;/p&gt;

&lt;p&gt;FalkorDB side. LiteLLM returns usage and exposes litellm.completion_cost, but I didn't want two different price sheets (one from LiteLLM's snapshot, one for Neo4j's raw OpenAI usage). I subclassed LiteLLM and LiteLLMEmbedder, overrode ainvoke / ainvoke_messages / _raw_embed_async, captured usage from each call, and sent the numbers through a single price sheet:&lt;/p&gt;

&lt;h1&gt;
  
  
  bench/costs.py — the whole thing.
&lt;/h1&gt;

&lt;p&gt;PRICES = {&lt;br&gt;
    "openai/gpt-4o-mini":      {"in": 0.15, "out": 0.60},   # per 1M tokens&lt;br&gt;
    "openai/gpt-4o":           {"in": 2.50, "out": 10.00},&lt;br&gt;
    "openai/text-embedding-3-small": {"in": 0.02},&lt;br&gt;
}&lt;br&gt;
Neo4j side. This is ugly. neo4j-graphrag.OpenAILLM.LLMResponse doesn't expose token usage at all. The underlying OpenAI client has it on the response object, but the wrapper drops it. So I subclassed OpenAILLM and monkey-patched the client:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;class TrackingOpenAILLM(OpenAILLM):&lt;br&gt;
    def __init__(self, *a, **kw):&lt;br&gt;
        super().__init__(*a, **kw)&lt;br&gt;
        self.call_count = 0; self.prompt_tokens = 0; self.completion_tokens = 0&lt;br&gt;
        sync_create = self.client.chat.completions.create&lt;br&gt;
        def sync_wrap(**kwargs):&lt;br&gt;
            r = sync_create(**kwargs)&lt;br&gt;
            self._record(r, self.model_name)&lt;br&gt;
            return r&lt;br&gt;
        self.client.chat.completions.create = sync_wrap  # same for async_client&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Same trick for OpenAIEmbeddings.client.embeddings.create. This is the ugliest code in the repo and I'm at peace with it — it's a benchmark harness, not a library. Both stacks' numbers now go through the same bench/costs.py. No drift, no surprises.&lt;/p&gt;

&lt;p&gt;Part 5 — The 25-question set and the judge&lt;/p&gt;

&lt;p&gt;I wrote 25 questions in four categories:&lt;/p&gt;

&lt;p&gt;Factual (8): single-hop lookups. "What is GraphRAG?" "What ports does FalkorDB expose?"&lt;br&gt;
Multi-hop (6): joins across entities or documents. "Which FalkorDB integrations does Securin use together?"&lt;br&gt;
Comparative (5): "How does FalkorDB compare to Neo4j for knowledge graphs?" "In-memory vs on-disk tradeoffs?"&lt;br&gt;
Numeric (6): specific numbers from the corpus. "What was Securin's average query latency?" "When is the cybersecurity webinar?"&lt;br&gt;
Every question is paired with reference_facts (the ground truth from the source document) and expected_source_docs (which files should be hit). The dataclass is frozen and asserts exactly 25 with unique IDs, so you can't silently drift the set.&lt;/p&gt;

&lt;p&gt;The judge lives in bench/judge.py. It's blind A/B:&lt;/p&gt;

&lt;h1&gt;
  
  
  Blind pairing so the judge never sees "FalkorDB" vs "Neo4j".
&lt;/h1&gt;

&lt;p&gt;rng = random.Random(42)&lt;br&gt;
for q in questions:&lt;br&gt;
    a, b = (falk, neo) if rng.random() &amp;lt; 0.5 else (neo, falk)&lt;br&gt;
    # ... send judge Answer A vs Answer B ...&lt;br&gt;
Rubric: four dimensions (groundedness, correctness, completeness, conciseness), integer 1–5 per dimension per answer, plus a one-sentence rationale. The judge is given the reference_facts from the question set, so the scoring is grounded in known-correct content, not in the judge's vibes about the question. The judge is gpt-4o with temperature=0, seed=42, response_format={"type": "json_object"}. Running the 25-question rubric cost $0.0456.&lt;/p&gt;

&lt;p&gt;Every run writes a timestamped JSON under results/ so I can re-render the comparison report later without re-running.&lt;/p&gt;

&lt;p&gt;Part 6 — The numbers&lt;/p&gt;

&lt;p&gt;Three tables. This is the whole benchmark, stripped of narration.&lt;/p&gt;

&lt;p&gt;Ingestion&lt;br&gt;
Metric  FalkorDB    Neo4j&lt;br&gt;
Wall time   233.4 s 251.7 s&lt;br&gt;
LLM calls   160 159&lt;br&gt;
Input / output / embedding tokens   112 925 / 60 211 / 43 759   172 419 / 28 208 / 35 130&lt;br&gt;
Cost    $0.0539 $0.0435&lt;br&gt;
Nodes   504 785&lt;br&gt;
Edges   1 202   1 632&lt;br&gt;
Entities / chunks   335 / 160   612 / 159&lt;br&gt;
Relationship types  154 226&lt;/p&gt;

&lt;p&gt;FalkorDB's extractor produces a tighter graph (fewer entities, fewer rel types); Neo4j's produces more fragments. Neither is better in the abstract — different defaults. FalkorDB reads less, writes more output tokens; Neo4j reads more (more prompt context per extraction call), writes less.&lt;/p&gt;

&lt;p&gt;Per-query aggregates (25 questions)&lt;br&gt;
Metric  FalkorDB    Neo4j&lt;br&gt;
Avg retrieve ms 1 493   496&lt;br&gt;
Avg LLM ms  1 641   1 759&lt;br&gt;
Avg total ms    3 094   2 255&lt;br&gt;
Avg LLM calls per Q 2.0 1.0&lt;br&gt;
Avg input / output tokens   4 125 / 59  2 952 / 55&lt;br&gt;
Avg cost per Q  $0.000654   $0.000476&lt;br&gt;
p95 latency 4 793 ms    4 506 ms&lt;br&gt;
Avg retrieved entities / chunks / docs  11.4 / 1.0 / 3.5    3.7 / 10.0 / 2.6&lt;br&gt;
25-Q total cost $0.01635    $0.01191&lt;/p&gt;

&lt;p&gt;Neo4j wins latency (−27 %) and cost (−27 %). Structurally this is because Neo4j does one LLM call per query and FalkorDB does two — keyword extraction, then generation, with deterministic ranking in between. The difference is not a configuration bug; it is what the SDK is doing on your behalf.&lt;/p&gt;

&lt;p&gt;Judge rubric (gpt-4o, seed=42, blind A/B)&lt;/p&gt;

&lt;p&gt;Dimension   FalkorDB    Neo4j   Δ&lt;br&gt;
Groundedness    3.88    3.84    +0.04&lt;br&gt;
Correctness 3.84    3.60    +0.24&lt;br&gt;
Completeness    3.52    3.24    +0.28&lt;br&gt;
Conciseness 4.56    4.60    −0.04&lt;br&gt;
Overall 3.95    3.82    +0.13&lt;br&gt;
Category    n   FalkorDB    Neo4j&lt;br&gt;
Factual 8   4.38    4.00&lt;br&gt;
Multi-hop   6   3.83    3.50&lt;br&gt;
Numeric 6   3.92    3.92&lt;br&gt;
Comparative 5   3.45    3.80&lt;/p&gt;

&lt;p&gt;Win/loss/tie over 25 questions (tie threshold |Δ| ≤ 0.125): 5 / 5 / 15 — tied on wins, but FalkorDB has the higher overall mean. Source-document recall is 97 % vs 90 % in FalkorDB's favour.&lt;/p&gt;

&lt;p&gt;Reframe, plainly: FalkorDB pays roughly 27 % more latency and 27 % more cost per query. In exchange it wins factual (+0.38) and multi-hop (+0.33) quality, leads on correctness (+0.24) and completeness (+0.28), and retrieves the right source document more often (97 % vs 90 %). It loses comparative questions (−0.35) where its pipeline tends to over-elaborate, and ties numeric extraction — both stacks get the same 4/6 numbers right and fail the same 2. The extra LLM call isn't free, but it's doing work on the substantive categories.&lt;/p&gt;

&lt;p&gt;Part 7 — Where each one actually failed&lt;/p&gt;

&lt;p&gt;Aggregates hide the interesting failures.&lt;/p&gt;

&lt;p&gt;FalkorDB's worst: the comparative category. On c2 ("How does GraphRAG outperform vector RAG on complex questions?") and c3 ("What are the tradeoffs of in-memory vs on-disk graph storage?") FalkorDB produced longer, more elaborated answers than Neo4j. The elaborations weren't fabricated — they were grounded in the retrieved entities — but they drifted past the reference facts the judge was scoring against. Neo4j's tighter single-pass answers hewed closer to exactly what the source said and won both questions. Average comparative score: FalkorDB 3.45 vs Neo4j 3.80.&lt;/p&gt;

&lt;p&gt;Lesson: the keyword-extraction pre-step pulls a wider entity set into context, which helps on factual and multi-hop questions but can encourage over-explanation on contrast questions where terseness is a virtue. The extra call is doing work; on some categories that work is counterproductive.&lt;/p&gt;

&lt;p&gt;Neo4j's worst: abstention on broad multi-hop and numeric questions. On m4 ("Which companies or products are described as using or integrating with FalkorDB?") and n5 ("What are FalkorDB's default ports?") Neo4j returned "I don't know based on the provided context." FalkorDB answered both — correctly naming Snowflake and LangChain on m4, though it happened to fail n5 on this run too (retrieval variance; the context didn't surface the ports chunk). In general Neo4j's retriever had the information in context and the single-pass prompt declined to use it.&lt;/p&gt;

&lt;p&gt;This is the flip side of no rewrite loop. FalkorDB's extra keyword-extraction call pushes the model to use what was retrieved; Neo4j's cautious single prompt occasionally refuses when a broader context pull would have landed the answer.&lt;/p&gt;

&lt;p&gt;I want to state this plainly: fabrication and abstention are both real failure modes. Neither is strictly worse. In a production system you'd probably tune the prompts to move each one toward the safer behavior for your use case. The point is not that one stack is wrong — it's that they fail differently.&lt;/p&gt;

&lt;p&gt;The asymmetries I did not fix&lt;br&gt;
Three of them, and I called every one out in the repo, in COMPARISON_FULL.md, and I'll call them out here too.&lt;/p&gt;

&lt;p&gt;Typed-edge promotion runs only on FalkorDB. Porting postprocess.py to Neo4j would have given Neo4j a cleanup its pipeline doesn't provide. I chose to benchmark defaults.&lt;/p&gt;

&lt;p&gt;Retrieval shape differs. FalkorDB's MultiPathRetrieval returns ~11 entities + 1 chunk per query. My Neo4j composite returns ~4 entities + 10 chunks. Both are tunable; I left them at reasonable defaults for each side. This likely explains part of FalkorDB's edge on multi-hop.&lt;/p&gt;

&lt;p&gt;Two LLM calls vs one. I did not strip out FalkorDB's keyword-extraction pre-step to "match" Neo4j. It's what the SDK does by default, and measuring the SDK means measuring that work.&lt;br&gt;
If you want the benchmark to tell a different story, you can rerun it with adjusted parameters. The harness is ~500 lines and the whole comparison costs fifteen cents.&lt;/p&gt;

&lt;p&gt;What I'd do differently&lt;/p&gt;

&lt;p&gt;Start with the benchmark harness, not the demo. The demo's code shaped itself around "five cool queries" and I ended up rewriting half of it when the 25-question set arrived. The right order is: questions → stacks → demo as a special case.&lt;br&gt;
Put bench/costs.py in from day one. I burned time reconciling LiteLLM's cost calc with the Neo4j-side raw usage before I realized a single price dict would erase the drift entirely.&lt;br&gt;
Expose community summaries in FalkorDB's retrieval. finalize() generates them but MultiPathRetrieval doesn't surface them on short queries. A custom retriever that includes community summaries for broad thematic questions (where the multi-hop expansion doesn't cover the space) is probably worth 15 minutes.&lt;br&gt;
Add a "refusal" dimension to the judge rubric (or a fifth score). Right now "I don't know" scores 1/5 on correctness, which is mathematically right — it isn't correct — but doesn't distinguish hallucination from honest abstention. A production benchmark should treat those separately.&lt;br&gt;
Use gpt-4o-mini as the judge on a larger sample. gpt-4o on 25 questions is fine for signal; gpt-4o-mini on 250 questions would probably be noisier per-question but more robust in aggregate, for the same budget.&lt;/p&gt;

&lt;p&gt;A note on the FalkorDB skills pack&lt;br&gt;
One thing that shaped how I worked on this: the repo ships a .falkordb-skills/ pack — SKILL.md plus cypher-skills/, operations-skills/, and udf-skills/ subfolders, each containing narrow, tested "how to do X in FalkorDB" notes. Copilot loads them automatically when I'm writing Cypher or operating the container. They're not tutorials; they're a short, opinionated reference for the things that are easy to get wrong.&lt;/p&gt;

&lt;p&gt;A few places they saved me time — or saved me from shipping something subtly broken:&lt;/p&gt;

&lt;p&gt;use-merge-to-avoid-duplicates and update-and-remove-properties. My postprocess.py rewrites relationships on every re-ingest. The skill pack is explicit that FalkorDB has no REMOVE clause (set to NULL instead) and that MERGE is the idiom for idempotent upserts. Both shaped the final shape of the typed-edge promotion code.&lt;br&gt;
use-parameterized-queries. The same skill is what prompted me to pass rel_type as a parameter (params={"t": raw}) while interpolating the sanitized type name into the Cypher. One is untrusted data; the other is part of the query structure. The skill makes the distinction concrete.&lt;/p&gt;

&lt;p&gt;track-slow-queries (GRAPH.SLOWLOG). This is how I eventually pinned down that the SDK makes two LLM calls per query — keyword extraction and generation — not three as I initially thought (my benchmark harness was double-counting a call; see the addendum). I wasn't looking for the call count at all; I was looking at what was slow during ingest. GRAPH.SLOWLOG also surfaced the real ingest bottleneck: a single UNWIND $batch AS item MATCH (e:&lt;strong&gt;Entity&lt;/strong&gt; {id:item.eid}) SET e.embedding = vecf32(item.vector) at ~333 ms per call, which is finalize() backfilling entity embeddings. Knowing the bottleneck is the embedding write, not the extraction, changes which optimizations are worth attempting.&lt;br&gt;
inspect-graphs-and-memory (GRAPH.LIST / GRAPH.INFO / GRAPH.MEMORY USAGE). This is what query_demo.py uses to print that the whole 509-node / 1 228-edge graph is 4 MB resident. That number is genuinely useful for capacity planning; it's also the kind of thing that's easy to forget to measure.&lt;br&gt;
apply-cypher-limitations-correctly. Specifically: &amp;lt;&amp;gt; filters aren't index-accelerated. I avoided at least one "let me just exclude this one type" query in postprocess.py that would have degraded on larger graphs.&lt;/p&gt;

&lt;p&gt;inspect-query-plans / profile-query-runtime. GRAPH.EXPLAIN and GRAPH.PROFILE. Not cited explicitly in the demo, but referenced in my .github/copilot-instructions.md so that any Cypher generated in this workspace is validated against an explain plan before being considered optimized.&lt;/p&gt;

&lt;p&gt;The value of the pack, to me, is less that it teaches me Cypher — I can read the docs — and more that it encodes the operational patterns that distinguish a working demo from a working system. Idempotency, introspection, index-awareness, parameter safety. A lot of LLM-generated Cypher looks fine and is quietly not idempotent or quietly scans a whole table. Having the skills loaded means the assistant writes code that wouldn't embarrass me in a review.&lt;/p&gt;

&lt;p&gt;If you adopt the SDK, copy the skills pack too. It's in the repo under .falkordb-skills/, MIT-licensed.&lt;/p&gt;

&lt;p&gt;Who actually needs this&lt;br&gt;
FalkorDB's graphrag-sdk is not the right tool for every retrieval problem. Being concrete about who it is for:&lt;/p&gt;

&lt;p&gt;You should reach for it when:&lt;/p&gt;

&lt;p&gt;Your corpus has implicit structure the model has to discover — case studies, research reports, customer-support tickets, product documentation, threat-intel feeds. Anything where "what relates to what" isn't already a table. The SDK's open-schema extraction is how you turn that into a graph without designing the graph yourself.&lt;/p&gt;

&lt;p&gt;Your questions regularly span multiple documents or require joining facts — "which of our customers using product X are on plan tier Y and had an incident last month?" The multi-hop and numeric wins on the judge rubric weren't theoretical; they were on exactly these question shapes.&lt;/p&gt;

&lt;p&gt;You need provenance on every answer — "which document did that claim come from?" The SDK tracks chunk provenance end-to-end; my query_demo.py prints it. This is the baseline for anything regulated or customer-facing.&lt;/p&gt;

&lt;p&gt;You're a Python team shipping a chat or agent feature and you do not want to stand up a separate Neo4j instance, a separate vector DB, and a separate ingestion pipeline. FalkorDB runs as a single container alongside your cache (it is a Redis module). The whole stack for this demo is docker compose up -d.&lt;br&gt;
You want to keep the graph small and fast — in-memory graph, 4 MB resident for this corpus, sub-millisecond single-hop Cypher. This matters when the graph is inline with a request path, not a nightly batch job.&lt;/p&gt;

&lt;p&gt;It's less obviously a fit when:&lt;/p&gt;

&lt;p&gt;Your "knowledge base" is actually a structured database (SaaS CRM, an ERP) where the relationships are already explicit. A graph projection over SQL, or a text-to-SQL pipeline, is a shorter path. The SDK's extraction cost is wasted on content you already have structured.&lt;br&gt;
Your workload is pure short-factual lookups where sub-second latency matters more than nuance. Neo4j's single-pass pipeline wins on latency and cost; vector RAG alone would win by even more. Do not pay for a keyword-extraction pre-pass if the question is "what's the phone number on page 3."&lt;br&gt;
You need a globally-distributed, multi-region write path. FalkorDB is single-instance or primary-replica. Fine for most apps; not for multi-region active-active.&lt;br&gt;
Your corpus is so large (hundreds of GB) that in-memory is not an option. FalkorDB can persist to disk, but the design center is "graph fits comfortably in RAM."&lt;br&gt;
If I had to describe the ideal user in one sentence: a Python developer building an agent, copilot, or customer-facing Q&amp;amp;A feature over a few hundred MB to a few GB of unstructured domain content, who needs multi-hop reasoning with source citations, wants ~60 lines of pipeline code to do most of the work, and cares more about being right than being the fastest responder by 300 ms.&lt;/p&gt;

&lt;p&gt;If that's you, the ~$0.05 ingest and ~$0.00065 per-query numbers from this benchmark are the shape you'll see in production. If that's not you, use something else — and the same harness in this repo will tell you that honestly.&lt;/p&gt;

&lt;p&gt;Running it yourself&lt;/p&gt;

&lt;p&gt;Everything is in the repo: the 8 Markdown files, the 25-question set, the judge prompt, all four raw-result JSON directories, the auto-rendered COMPARISON.md, and the hand-written COMPARISON_FULL.md with per-question transcripts and judge rationales.&lt;/p&gt;

&lt;p&gt;git clone &lt;a href="https://github.com/FalkorDB/graphrag-sdk-demo" rel="noopener noreferrer"&gt;https://github.com/FalkorDB/graphrag-sdk-demo&lt;/a&gt;&lt;br&gt;
cd graphrag-sdk-demo&lt;br&gt;
cp .env.example .env &amp;amp;&amp;amp; $EDITOR .env    # OPENAI_API_KEY&lt;br&gt;
docker compose up -d&lt;br&gt;
python3.14 -m venv .venv &amp;amp;&amp;amp; .venv/bin/pip install -r requirements.txt&lt;br&gt;
.venv/bin/python benchmark_compare.py all&lt;br&gt;
About fifteen minutes, about fifteen cents.&lt;/p&gt;

&lt;p&gt;If you want a recommendation, here's the one I'm willing to commit to: if your workload is dominated by factual completeness and multi-hop reasoning where correctness and source recall matter more than latency, FalkorDB's extra keyword-extraction pass is paying for something measurable (+0.38 factual, +0.33 multi-hop, 97 % vs 90 % source-doc recall, +0.24 correctness, +0.28 completeness). If your workload is latency- or cost-sensitive — especially short comparative questions where Neo4j's tighter single-pass prompt actually wins, or numeric extraction where the two stacks tie — Neo4j is 27 % faster and 27 % cheaper and gives equivalent answers.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>falkordb</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Text2SQL on xxxx's of tables?</title>
      <dc:creator>Dan Shalev</dc:creator>
      <pubDate>Mon, 05 Jan 2026 13:56:33 +0000</pubDate>
      <link>https://dev.to/danshalev7/text2sql-on-xxxxs-of-tables-4775</link>
      <guid>https://dev.to/danshalev7/text2sql-on-xxxxs-of-tables-4775</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7khs4z1till7adolazzn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7khs4z1till7adolazzn.png" alt="QueryWeaver text-to-sql interface" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Text-to-SQL tools work fine in demos. Production with 30K+ tables? They hallucinate relationships and fail in ways you can't debug because the code is closed.&lt;/p&gt;

&lt;p&gt;We've shipped several updates focused on solving this.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's New
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;React UI Rebuild&lt;/strong&gt;&lt;br&gt;
Cleaned up the frontend, fixed button logic issues, improved overall UX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API Release&lt;/strong&gt;&lt;br&gt;
New API endpoint for programmatic access. Full docs in the repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend Performance Improvements&lt;/strong&gt;&lt;br&gt;
Optimized graph generation and query processing for large-scale schemas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full Schema Auditability&lt;/strong&gt;&lt;br&gt;
You can now inspect exactly how your schema gets mapped into the knowledge graph—see the indexing logic, relationship detection, everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture Difference
&lt;/h3&gt;

&lt;p&gt;Vector embeddings can't capture the semantic depth needed for complex relational schemas. QueryWeaver uses knowledge graphs to map table relationships structurally, not statistically. Less hallucination, better accuracy and subsequent queries.&lt;/p&gt;

&lt;p&gt;Check it out, it's free: &lt;a href="https://app.queryweaver.ai/" rel="noopener noreferrer"&gt;https://app.queryweaver.ai/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>sql</category>
      <category>falkordb</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Graph to store your security data?</title>
      <dc:creator>Dan Shalev</dc:creator>
      <pubDate>Sun, 17 Aug 2025 06:15:15 +0000</pubDate>
      <link>https://dev.to/falkordb/graph-to-store-your-security-data-531l</link>
      <guid>https://dev.to/falkordb/graph-to-store-your-security-data-531l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The Challenge: You run a multi-tenant security platform and need to ensure full tenant isolation, avoiding customers' data commingling in the same database or needing to spin up a dedicated database for every new customer.&lt;/p&gt;

&lt;p&gt;How it affects you: You either introduce risk of data leakage or waste infrastructure resources on isolated stacks.&lt;/p&gt;

&lt;p&gt;Why choose graph: You can manage 10,000+ isolated graph tenants per database (across all pricing tiers). Each tenant gets a private namespace and query surface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🟰 Business impact: Zero tenant data commingling. Minimal DevOps overhead. Efficient scaling of your infrastructure as you grow.&lt;/p&gt;

&lt;p&gt;The other 5 reasons are &lt;a href="https://www.falkordb.com/blog/store-security-data-in-a-graph/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>graph</category>
      <category>database</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Graph database v4.10 is out!</title>
      <dc:creator>Dan Shalev</dc:creator>
      <pubDate>Thu, 12 Jun 2025 14:02:00 +0000</pubDate>
      <link>https://dev.to/falkordb/graph-database-v410-is-out-883</link>
      <guid>https://dev.to/falkordb/graph-database-v410-is-out-883</guid>
      <description>&lt;p&gt;The new release (v4.10.0) is out, and I wanted to share some of the updates and ask for feedback from folks who care about performance, memory efficiency in graph-heavy systems.&lt;br&gt;
FalkorDB is an open-source property graph database that supports OpenCypher (with our own extensions) and is used under the hood for retrieval-augmented generation setups where accuracy matters.&lt;br&gt;
The big problem we’re working on is scaling graph databases without memory bloat or unpredictable performance in prod. Support for Indexing tends to be limited with array fields. And if you want to do something basic like compare a current value to the previous one in a sequence (think time series modeling), the query engine often makes you jump through hoops.&lt;br&gt;
We started FalkorDB after working for years on RedisGraph (we were the original authors). Rather than patch the old codebase, we built FalkorDB with a sparse matrix algebra backend for performance. Our goal was to build something that could hold up under pressure, like 10K+ graphs in a single instance, and still let you answer complex queries interactively.&lt;br&gt;
To get closer to this goal, we’ve added the following improvements in this new version: We added string interning with a new intern() function. It lets you deduplicate identical strings across graphs, which is surprisingly useful in, for example, recommender systems where you have millions of “US” strings. We also added a command (GRAPH.MEMORY USAGE) that breaks down memory consumption by nodes, edges, matrices, and indices (per graph), which is useful when you’re trying to figure out if your heap is getting crushed by edge cardinality or indexing overhead.&lt;br&gt;
Indexing got smarter too, with arrays now natively indexable in a way that’s actually usable in production (Neo4j doesn’t do this natively, last I checked). &lt;br&gt;
On the analytics side, we added CDLP (community detection via label propagation), WCC (weakly connected components), and betweenness centrality, which are all exposed as procedures. These came out of working with teams in fraud detection and behavioral clustering where you don’t want to guess the number of communities in advance.&lt;br&gt;
If you want to try FalkorDB, we recommend you run it via Docker&lt;br&gt;
The code’s also available on GitHub (&lt;a href="https://github.com/FalkorDB/falkordb" rel="noopener noreferrer"&gt;https://github.com/FalkorDB/falkordb&lt;/a&gt;) and we have a live sandbox you can play with at &lt;a href="https://browser.falkordb.com" rel="noopener noreferrer"&gt;https://browser.falkordb.com&lt;/a&gt;. No login or install needed to run queries. Docs are at &lt;a href="https://docs.falkordb.com" rel="noopener noreferrer"&gt;https://docs.falkordb.com&lt;/a&gt;. &lt;br&gt;
Curious to hear from anyone who’s building graph-heavy systems, especially if you’ve hit memory or indexing limits elsewhere. We’re heads-down building and always learning, grateful for any feedback or test cases you throw at us.&lt;/p&gt;

</description>
      <category>database</category>
    </item>
    <item>
      <title>How to use a knowledge graph ft. Yohei Nakajima</title>
      <dc:creator>Dan Shalev</dc:creator>
      <pubDate>Tue, 27 May 2025 11:29:14 +0000</pubDate>
      <link>https://dev.to/falkordb/how-to-use-a-knowledge-graph-ft-yohei-nakajima-2mf7</link>
      <guid>https://dev.to/falkordb/how-to-use-a-knowledge-graph-ft-yohei-nakajima-2mf7</guid>
      <description>&lt;p&gt;​In this workshop we’ll show 2 live builds: Fractal KG, a UI for building knowledge graphs from a natural language prompt, and VCPedia, a real-time startup intelligence Crunchbase-like platform that is graph powered by hourly Twitter pulls, LLM-based funding-round extraction, and automated newsletters.&lt;/p&gt;

&lt;p&gt;Get information: Map out Fractal KG’s architecture: ingestion, embedding, dedupe, hierarchy, and newsletters.&lt;br&gt;
Get inspired: Break down VCPedia’s architecture.&lt;br&gt;
Get started: Integrate FalkorDB for graph queries, multi-tenant support, and setup.&lt;/p&gt;

&lt;p&gt;18 June, 2025 ⏰ PDT: 10:00 AM | EDT: 1:00 PM | CEST: 7:00 PM&lt;/p&gt;

&lt;p&gt;Sign up link: &lt;a href="https://lu.ma/192rvxcg" rel="noopener noreferrer"&gt;Sign up here (free)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;p.s you can register and we’ll send a recap (even if you can’t attend).&lt;/p&gt;

</description>
      <category>llm</category>
      <category>rag</category>
      <category>graphql</category>
    </item>
    <item>
      <title>Questions to ask before you build a knowledge graph</title>
      <dc:creator>Dan Shalev</dc:creator>
      <pubDate>Tue, 06 May 2025 08:10:45 +0000</pubDate>
      <link>https://dev.to/falkordb/questions-to-ask-before-you-build-a-knowledge-graph-47ff</link>
      <guid>https://dev.to/falkordb/questions-to-ask-before-you-build-a-knowledge-graph-47ff</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Are you planning to develop intelligent chatbots that require advanced understanding and interaction capabilities?&lt;/li&gt;
&lt;li&gt;Is your focus on enabling dynamic, complex research endeavors?&lt;/li&gt;
&lt;li&gt;Do you want to visualize or monitor asset flows and risks within your organization?&lt;/li&gt;
&lt;li&gt;Do you aim to unlock siloed data or enhance connectivity between disparate data environments?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.falkordb.com/blog/how-to-build-a-knowledge-graph/" rel="noopener noreferrer"&gt;Knowledge graphs&lt;/a&gt; help structure information by capturing relationships between disparate data points. They allow users to integrate data from diverse sources and discover hidden patterns and connections.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why I Fell in Love with Rust Procedural Macros</title>
      <dc:creator>Dan Shalev</dc:creator>
      <pubDate>Mon, 05 May 2025 13:14:31 +0000</pubDate>
      <link>https://dev.to/falkordb/why-i-fell-in-love-with-rust-procedural-macros-49mh</link>
      <guid>https://dev.to/falkordb/why-i-fell-in-love-with-rust-procedural-macros-49mh</guid>
      <description>&lt;p&gt;I consider myself a junior Rust developer. I have been learning Rust for a few months now, and I have thoroughly enjoyed&lt;br&gt;
the process. Recently, we started writing the &lt;a href="https://github.com/FalkorDB/falkordb-rs-next-gen" rel="noopener noreferrer"&gt;next generation&lt;/a&gt; of&lt;br&gt;
Falkordb using Rust. We chose Rust because of its&lt;br&gt;
performance, safety, and rich type system.&lt;/p&gt;

&lt;p&gt;One part we are implementing by hand is the scanner and parser. We do this to optimize performance and to maintain a&lt;br&gt;
clean AST (abstract syntax tree). We are working with&lt;br&gt;
the &lt;a href="https://github.com/FalkorDB/falkordb-rs-next-gen/blob/main/graph/src/Cypher.g4" rel="noopener noreferrer"&gt;Antlr4 Cypher grammar&lt;/a&gt;, where each Derivation in the grammar maps to a Rust function.&lt;/p&gt;

&lt;p&gt;For example, consider the parse rule for a NOT expression:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;oC_NotExpression&lt;br&gt;
: ( NOT SP? )* oC_ComparisonExpression ;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This corresponds to the Rust function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;parse_not_expr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;QueryExprIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;not_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Not&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;not_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.parse_comparison_expr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;not_count&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;QueryExprIR&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we compress consecutive NOT expressions during parsing, but otherwise, the procedure closely resembles the Antlr4&lt;br&gt;
grammar. The function first consumes zero or more NOT tokens, then calls parse_comparison_expr&lt;/p&gt;

&lt;p&gt;While working on the parser, a recurring pattern emerged. Many expressions follow the form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;oC_ComparisonExpression
: oC_OrExpression ( ( SP? COMPARISON_OPERATOR SP? ) oC_OrExpression )* ;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which translates roughly to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;parse_comparison_expr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;QueryExprIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.parse_or_expr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ComparisonOperator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.current&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.parse_or_expr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;QueryExprIR&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;BinaryOp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly, for addition and subtraction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;oC_AddOrSubtractExpression
: oC_MultiplyDivideModuloExpression ( ( SP? '+' SP? oC_MultiplyDivideModuloExpression ) | ( SP? '-' SP? oC_MultiplyDivideModuloExpression ) )* ;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which looks like this in Rust:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;parse_add_sub_expr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;QueryExprIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.parse_mul_div_modulo_expr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Plus&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.parse_mul_div_modulo_expr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="n"&gt;vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;QueryExprIR&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Dash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.parse_mul_div_modulo_expr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="n"&gt;vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;QueryExprIR&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Plus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Dash&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.current&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="nf"&gt;.pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern appeared repeatedly with one, two, or three operators. Although the code is not very complicated, it would&lt;br&gt;
be nice to have a macro that generates this code for us.&lt;/p&gt;

&lt;p&gt;We envisioned a macro that takes the expression parser and pairs of (token, AST constructor) like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;parse_binary_expr!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.parse_mul_div_modulo_expr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Plus&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dash&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I started exploring how to write procedural macros in Rust, and I must say it was a very pleasant experience. With&lt;br&gt;
the help of the crates quote and syn, I was able to write a procedural macro that generates this code automatically. The&lt;br&gt;
quote crate lets you generate token streams from templates, and syn allows parsing Rust code into syntax trees and token&lt;br&gt;
streams. Using these two crates makes writing procedural macros in Rust feel like writing a compiler extension.&lt;/p&gt;

&lt;p&gt;Let's get into the code.&lt;/p&gt;

&lt;p&gt;The first step is to model your macro syntax using Rust data structures. In our case, I used two structs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;BinaryOp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="n"&gt;parse_exp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Expr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;binary_op_alts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BinaryOpAlt&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;BinaryOpAlt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="n"&gt;token_match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;syn&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Ident&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;ast_constructor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;syn&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Ident&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The leaves of these structs are data types from the syn crate. Expr represents any Rust expression, and syn::Ident&lt;br&gt;
represents an identifier.&lt;/p&gt;

&lt;p&gt;Next, we parse the token stream into these data structures. This is straightforward with syn by implementing the Parse&lt;br&gt;
trait:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Parse&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;BinaryOp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ParseStream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;parse_exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="py"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;syn&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;Token!&lt;/span&gt;&lt;span class="p"&gt;[,]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;binary_op_alts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nn"&gt;syn&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;punctuated&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Punctuated&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BinaryOpAlt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;syn&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;Token!&lt;/span&gt;&lt;span class="p"&gt;[,]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;parse_separated_nonempty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="n"&gt;parse_exp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;binary_op_alts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;binary_op_alts&lt;/span&gt;&lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Parse&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;BinaryOpAlt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ParseStream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;token_match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="py"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;syn&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;Token!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ast_constructor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="n"&gt;token_match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;ast_constructor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The syn crate smartly parses the token stream into the data structures based on the expected types (Token, Expr, Ident,&lt;br&gt;
or BinaryOpAlt).&lt;/p&gt;

&lt;p&gt;The final step is to generate the appropriate code from these data structures using the quote crate, which lets you&lt;br&gt;
write Rust code templates that generate token streams. This is done by implementing the ToTokens trait:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="nn"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ToTokens&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;BinaryOp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;to_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="nn"&gt;proc_macro2&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TokenStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;binary_op_alts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.binary_op_alts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;parse_exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.parse_exp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_token_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parse_exp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;binary_op_alts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="nf"&gt;.extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;generate_token_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;parse_exp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Expr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;alts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BinaryOpAlt&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;proc_macro2&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TokenStream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;whiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alts&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;token_match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="py"&gt;.token_match&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ast_constructor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="py"&gt;.ast_constructor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nn"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;quote!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;#&lt;span class="n"&gt;token_match&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;#&lt;span class="n"&gt;parse_exp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="n"&gt;vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;QueryExprIR&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;#&lt;span class="nf"&gt;ast_constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alts&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;token_match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="py"&gt;.token_match&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nn"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;quote!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nn"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;#&lt;span class="n"&gt;token_match&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nn"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;quote!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;#&lt;span class="n"&gt;parse_exp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
#&lt;span class="p"&gt;(&lt;/span&gt;#&lt;span class="n"&gt;whiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;#&lt;span class="p"&gt;(&lt;/span&gt;#&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lexer&lt;/span&gt;&lt;span class="nf"&gt;.current&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="nf"&gt;.pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In generate_token_stream, we first generate the collection of while loops for each operator, then place them inside a&lt;br&gt;
loop using the repetition syntax &lt;code&gt;#(#whiles)*&lt;/code&gt;. And that's it!&lt;/p&gt;

&lt;p&gt;You can find the full code &lt;a href="https://github.com/FalkorDB/falkordb-rs-next-gen/tree/main/falkordb-macro" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
    </item>
    <item>
      <title>VectorRAG is naive, lacks domain awareness, and can’t handle full dataset retrieval</title>
      <dc:creator>Dan Shalev</dc:creator>
      <pubDate>Tue, 29 Apr 2025 12:13:40 +0000</pubDate>
      <link>https://dev.to/falkordb/vectorrag-is-naive-lacks-domain-awareness-and-cant-handle-full-dataset-retrieval-5g59</link>
      <guid>https://dev.to/falkordb/vectorrag-is-naive-lacks-domain-awareness-and-cant-handle-full-dataset-retrieval-5g59</guid>
      <description>&lt;p&gt;If we were building a GenAI stack today, we'd start with one question: Can your retrieval system handle multi-hop logic?&lt;/p&gt;

&lt;p&gt;Trick question, b/c most can’t. They treat retrieval as nearest-neighbor search.&lt;/p&gt;

&lt;p&gt;Today, we discussed scaling #&lt;a href="https://github.com/FalkorDB/GraphRAG-SDK/blob/main/README.md" rel="noopener noreferrer"&gt;GraphRAG&lt;/a&gt; at AWS DevOps Day, and the takeaway is clear: VectorRAG is naive, lacks domain awareness, and can’t handle full dataset retrieval. &lt;/p&gt;

&lt;p&gt;GraphRAG &lt;a href="https://falkordb.com/" rel="noopener noreferrer"&gt;builds a knowledge graph&lt;/a&gt; from source documents, allowing for a deeper understanding of the data + higher accuracy.&lt;/p&gt;

</description>
      <category>vectordatabase</category>
      <category>rag</category>
      <category>ai</category>
    </item>
    <item>
      <title>X0,000s Ops/sec with Multigraph Topology</title>
      <dc:creator>Dan Shalev</dc:creator>
      <pubDate>Wed, 23 Apr 2025 13:44:32 +0000</pubDate>
      <link>https://dev.to/falkordb/x0000s-opssec-with-multigraph-topology-4nea</link>
      <guid>https://dev.to/falkordb/x0000s-opssec-with-multigraph-topology-4nea</guid>
      <description>&lt;p&gt;Yesterday's 'Easily Achieve X0,000s Ops/sec with Multigraph Topology' workshop was awesome. Great questions, too! &lt;/p&gt;

&lt;p&gt;We started with a standalone machine with 16 cores = 26k queries per second. We tripled and then doubled the number of cores, and achieved linear scalability.&lt;/p&gt;

&lt;p&gt;Recording link:  &lt;a href="https://www.youtube.com/watch?v=LbeA0-xy1f8" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=LbeA0-xy1f8&lt;/a&gt;&lt;/p&gt;

</description>
      <category>performance</category>
      <category>discuss</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Your GenAI system is only as smart as its retrieval layer.</title>
      <dc:creator>Dan Shalev</dc:creator>
      <pubDate>Wed, 16 Apr 2025 11:20:32 +0000</pubDate>
      <link>https://dev.to/falkordb/your-genai-system-is-only-as-smart-as-its-retrieval-layer-2963</link>
      <guid>https://dev.to/falkordb/your-genai-system-is-only-as-smart-as-its-retrieval-layer-2963</guid>
      <description>&lt;p&gt;A recent enterprise GenAI survey just confirmed what we’ve been seeing in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;85% of teams are deploying LLMs&lt;/li&gt;
&lt;li&gt;71% are &lt;em&gt;already seeing output risks&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;99% agree: human oversight is still mandatory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s be clear—this isn’t about model tuning. It’s about &lt;em&gt;retrieval failure&lt;/em&gt; at the infrastructure level.&lt;/p&gt;

&lt;p&gt;You can’t generate correct answers if your stack can’t model relationships.&lt;/p&gt;

&lt;p&gt;You can’t trace decisions if your data lacks structure.&lt;/p&gt;

&lt;p&gt;You can’t scale trust if your system hallucinates under pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here’s what actually works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load your structured + unstructured knowledge into a graph database&lt;/li&gt;
&lt;li&gt;Model entities, relationships, and policies—don’t flatten them&lt;/li&gt;
&lt;li&gt;Route results into your LLM prompt—clean, fast, explainable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s how you replace retrieval duct tape with graph-native reasoning.&lt;/p&gt;

&lt;p&gt;Exploring advanced RAG &amp;amp; GraphRAG? Start here: &lt;a href="https://github.com/FalkorDB/GraphRAG-SDK" rel="noopener noreferrer"&gt;https://github.com/FalkorDB/GraphRAG-SDK&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Add a Knowledge Graph 3x better</title>
      <dc:creator>Dan Shalev</dc:creator>
      <pubDate>Wed, 09 Apr 2025 07:27:09 +0000</pubDate>
      <link>https://dev.to/falkordb/add-a-knowledge-graph-3x-better-24mb</link>
      <guid>https://dev.to/falkordb/add-a-knowledge-graph-3x-better-24mb</guid>
      <description>&lt;p&gt;If your AI agent doesn’t know when it’s wrong, it doesn’t belong in production.&lt;/p&gt;

&lt;p&gt;We reviewed a recent study that tested LLM pipelines against enterprise data environments, benchmarking their performance on enterprise datasets using the Yale Spider schema.&lt;/p&gt;

&lt;p&gt;Same model. Same questions. Different architecture.&lt;/p&gt;

&lt;p&gt;**Here’s what changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;**SQL-only → 17.28% accuracy&lt;/li&gt;
&lt;li&gt;Add a Knowledge Graph → 3x better&lt;/li&gt;
&lt;li&gt;Add ontology-based query checks + repair loop → 72.55% accuracy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's not incremental progress. That’s a systems-level shift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here’s what mattered most:
&lt;/h2&gt;

&lt;p&gt;70% of fixes came from domain constraints in the query body&lt;br&gt;
Most gains showed up in complex schema environments—think KPIs and strategic planning&lt;br&gt;
And when the model couldn’t repair itself? It admitted it with “unknown,” cutting hallucinated outputs by a huge margin&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture looks like this:
&lt;/h2&gt;

&lt;p&gt;Ontologies validate logic pre-execution&lt;br&gt;
Knowledge graphs serve as real-time reasoning layers&lt;br&gt;
LLM Repair loops handle failure cases autonomously&lt;br&gt;
&lt;a href="https://falkordb.com" rel="noopener noreferrer"&gt;FalkorDB&lt;/a&gt; is already solving the low-latency challenge here—serving graphs in real time for reasoning-heavy queries.&lt;br&gt;
The lesson: You don’t need smarter prompts. You need systems that can detect when the logic breaks—and fix it before it hits the user.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Vector Recall Reasoning</title>
      <dc:creator>Dan Shalev</dc:creator>
      <pubDate>Tue, 01 Apr 2025 13:47:41 +0000</pubDate>
      <link>https://dev.to/falkordb/vector-recall-reasoning-5cko</link>
      <guid>https://dev.to/falkordb/vector-recall-reasoning-5cko</guid>
      <description>&lt;p&gt;If your GenAI agent can’t reason across relationships, memory, and context—it’s not an agent. It’s a demo.&lt;/p&gt;

&lt;p&gt;We came across a research system called DEMENTIA-PLAN—focused on dementia care, but it exposed something bigger: the fatal flaw in most GenAI stacks (source in comments).&lt;/p&gt;

&lt;p&gt;Agents that run vector-only retrieval pipelines fail to answer questions grounded in human context.&lt;/p&gt;

&lt;p&gt;DEMENTIA-PLAN used multiple &lt;a href="https://www.falkordb.com/blog/kpmg-ai-report-graphrag-ai-agents/" rel="noopener noreferrer"&gt;knowledge graphs + a planning agent&lt;/a&gt; to adapt retrieval in real time. Result? 30% better memory support. 10% higher coherence.&lt;/p&gt;

&lt;p&gt;This isn’t just about healthcare.&lt;br&gt;
It’s a blueprint for every agent stack that actually needs to think. If your RAG pipeline gets retrieval wrong, you’re shipping guessware.&lt;/p&gt;

</description>
      <category>agentaichallenge</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
