<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Agent,Graphs, Models & Code]]></title><description><![CDATA[Building ML systems, one layer at a time — documenting the real process 
behind Insight Engine, a GraphRAG-powered research discovery platform. 
MLOps, SageMaker, knowledge graphs, and everything in between.]]></description><link>https://sumukha87.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/69c132db30a9b81e3a45fdd9/7146f621-4d1e-4ad4-a3f4-58776e6d877b.png</url><title>Agent,Graphs, Models &amp; Code</title><link>https://sumukha87.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 10 Nov 2026 10:21:11 GMT</lastBuildDate><atom:link href="https://sumukha87.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The NLP Pipeline Behind Insight Engine: Extracting 10.7M Entities from 229K Research Papers (Part 2)]]></title><description><![CDATA[How I used spaCy, SciSpacy, and a rule-based relation extractor to extract 10,779,699 entities and 2,294,895 relationships from 229,498 arXiv papers…]]></description><link>https://sumukha87.hashnode.dev/the-nlp-pipeline-behind-insight-engine-extracting-10-7m-entities-from-229k-research-papers-part-2</link><guid isPermaLink="true">https://sumukha87.hashnode.dev/the-nlp-pipeline-behind-insight-engine-extracting-10-7m-entities-from-229k-research-papers-part-2</guid><category><![CDATA[nlp]]></category><category><![CDATA[knowledge graph]]></category><category><![CDATA[DVCS]]></category><category><![CDATA[mlops]]></category><category><![CDATA[Build In Public]]></category><dc:creator><![CDATA[SriSumukha S]]></dc:creator><pubDate>Wed, 25 Mar 2026 10:09:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69c132db30a9b81e3a45fdd9/2aa66284-1afd-4752-89b9-1bfce5fa4b70.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This is Part 2 of <strong>Building Insight Engine</strong> — a build-in-public series documenting the real development process behind a GraphRAG-powered research discovery platform. <a href="https://sumukha87.hashnode.dev/building-insight-engine-how-i-m-using-graphrag-to-discover-cross-domain-innovation-part-1">Part 1 covers the full project overview and stack.</a></em></p>
<hr />
<h2>Why NLP Before the Graph</h2>
<p>The knowledge graph doesn't build itself. Before a single node lands in Neo4j, every paper has to go through two NLP stages: named entity recognition to identify <em>what</em> exists in the text, and relation extraction to identify <em>how</em> those things connect.</p>
<p>Get this wrong and the graph is noise. Get it right and you have a structured knowledge base that no keyword search can replicate.</p>
<p>Here's exactly how both stages work.</p>
<hr />
<h2>The Pipeline Structure</h2>
<p>The entire NLP workflow is a reproducible DVC pipeline with four tracked stages:</p>
<pre><code>fetch_arxiv → ner → relations → graph_loader
</code></pre>
<p>Every stage is version-controlled. If a parameter changes — confidence threshold, batch size, entity type mapping — DVC detects it, marks downstream stages stale, and re-runs only what's affected. Every run logs metrics to MLflow under the <code>insight-engine-nlp</code> experiment.</p>
<p>This means every entity count, relation count, and domain breakdown is reproducible and comparable across runs. If I tighten the confidence threshold tomorrow, I can immediately see the effect on graph density without touching the codebase.</p>
<hr />
<h2>Stage 1 — Data Ingestion</h2>
<p><strong>Script:</strong> <code>src/ingestion/arxiv_fetcher.py</code></p>
<p>The pipeline starts with the arXiv API. The fetcher queries 12 domain-specific search terms, pulls paper metadata — title, abstract, authors, categories, published date — and writes to JSONL files.</p>
<p>Three behaviours worth noting:</p>
<p>Deduplication is on <code>arxiv_id</code> — re-running the fetcher never downloads the same paper twice. New papers land in new batch files, which DVC detects as a change and marks the NER stage stale automatically. The fetcher is rate-limited to 3 seconds between requests per arXiv's API requirements.</p>
<p>Current result: <strong>229,498 papers</strong> across <strong>12 domains</strong> — Aerospace, Medical Devices, Materials, Energy, Biotechnology, Robotics, Quantum, Nanotechnology, Environment, Semiconductors, Pharma, and Neuroscience.</p>
<hr />
<h2>Stage 2 — Named Entity Recognition</h2>
<p><strong>Script:</strong> <code>src/nlp/ner_pipeline.py</code>
<strong>Model:</strong> <code>en_core_sci_lg</code> (SciSpacy large biomedical model)</p>
<p>The NER stage reads every paper's title and abstract and extracts named entities, mapping each to one of 12 canonical entity types:</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>What it captures</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><code>Technology</code></td>
<td>Methods, platforms, systems</td>
<td>"CRISPR", "lithography"</td>
</tr>
<tr>
<td><code>Material</code></td>
<td>Substances, alloys, nanomaterials</td>
<td>"titanium alloy", "graphene"</td>
</tr>
<tr>
<td><code>Disease</code></td>
<td>Conditions, disorders, syndromes</td>
<td>"Alzheimer's", "arrhythmia"</td>
</tr>
<tr>
<td><code>Device</code></td>
<td>Instruments, implants, sensors</td>
<td>"pacemaker", "transistor"</td>
</tr>
<tr>
<td><code>Compound</code></td>
<td>Chemicals, drugs, molecules</td>
<td>"aspirin", "lithium oxide"</td>
</tr>
<tr>
<td><code>Process</code></td>
<td>Manufacturing steps, bio processes</td>
<td>"sputtering", "fermentation"</td>
</tr>
<tr>
<td><code>Organism</code></td>
<td>Bacteria, viruses, cell types</td>
<td>"E. coli", "T-cell"</td>
</tr>
<tr>
<td><code>Gene</code></td>
<td>Genes, proteins, biomarkers</td>
<td>"BRCA1", "tau protein"</td>
</tr>
<tr>
<td><code>Algorithm</code></td>
<td>ML models, optimizers</td>
<td>"transformer", "ResNet"</td>
</tr>
<tr>
<td><code>Phenomenon</code></td>
<td>Physical/chemical effects</td>
<td>"superconductivity", "entanglement"</td>
</tr>
<tr>
<td><code>Software</code></td>
<td>Frameworks, libraries, tools</td>
<td>"PyTorch", "CUDA"</td>
</tr>
<tr>
<td><code>Infrastructure</code></td>
<td>Large-scale systems, facilities</td>
<td>"reactor", "wind farm"</td>
</tr>
</tbody></table>
<p><strong>Why <code>en_core_sci_lg</code> over the general English model?</strong></p>
<p>Scientific text is a different language. "Transformer" in a general corpus means a piece of electrical equipment. In a machine learning paper it's an architecture. SciSpacy's biomedical model has pre-trained representations for scientific vocabulary that the general model completely misses.</p>
<p><strong>Results:</strong> <strong>10,779,699 entity mentions</strong> extracted across 229,498 documents — an average of <strong>47 entities per document</strong>. Every run logs these counts to MLflow per domain, so I can immediately see if a new data batch skews the distribution.</p>
<hr />
<h2>Stage 3 — Relation Extraction (v1 Rule-Based)</h2>
<p><strong>Script:</strong> <code>src/nlp/relation_extractor.py</code></p>
<p>This is where the architecture gets interesting — and where I made a deliberate, opinionated tradeoff.</p>
<p>Relations in Insight Engine are not extracted by reading sentences. They are assigned by entity <em>type pairs</em>. If a <code>Material</code> and a <code>Device</code> co-occur in the same document, the rule table says: emit a <code>COMPOSED_OF</code> relationship with confidence 0.80.</p>
<p>Here's the full rule table:</p>
<table>
<thead>
<tr>
<th>Relation</th>
<th>Head Type</th>
<th>Tail Type</th>
<th>Confidence</th>
</tr>
</thead>
<tbody><tr>
<td><code>TREATS</code></td>
<td>Compound</td>
<td>Disease</td>
<td>0.85</td>
</tr>
<tr>
<td><code>TREATS</code></td>
<td>Device</td>
<td>Disease</td>
<td>0.75</td>
</tr>
<tr>
<td><code>CAUSES</code></td>
<td>Gene</td>
<td>Disease</td>
<td>0.80</td>
</tr>
<tr>
<td><code>CAUSES</code></td>
<td>Organism</td>
<td>Disease</td>
<td>0.75</td>
</tr>
<tr>
<td><code>INHIBITS</code></td>
<td>Compound</td>
<td>Gene</td>
<td>0.80</td>
</tr>
<tr>
<td><code>INHIBITS</code></td>
<td>Compound</td>
<td>Organism</td>
<td>0.75</td>
</tr>
<tr>
<td><code>COMPOSED_OF</code></td>
<td>Material</td>
<td>Device</td>
<td>0.80</td>
</tr>
<tr>
<td><code>COMPOSED_OF</code></td>
<td>Material</td>
<td>Infrastructure</td>
<td>0.75</td>
</tr>
<tr>
<td><code>DERIVED_FROM</code></td>
<td>Material</td>
<td>Process</td>
<td>0.75</td>
</tr>
<tr>
<td><code>SYNTHESIZED_FROM</code></td>
<td>Process</td>
<td>Material</td>
<td>0.80</td>
</tr>
<tr>
<td><code>IMPROVES</code></td>
<td>Technology</td>
<td>Technology</td>
<td>0.70</td>
</tr>
<tr>
<td><code>IMPROVES</code></td>
<td>Algorithm</td>
<td>Algorithm</td>
<td>0.70</td>
</tr>
<tr>
<td><code>USED_IN</code></td>
<td>Algorithm</td>
<td>Technology</td>
<td>0.75</td>
</tr>
<tr>
<td><code>USED_IN</code></td>
<td>Software</td>
<td>Technology</td>
<td>0.75</td>
</tr>
<tr>
<td><code>INTERACTS_WITH</code></td>
<td>Compound</td>
<td>Compound</td>
<td>0.70</td>
</tr>
<tr>
<td><code>REPLACES</code></td>
<td>Device</td>
<td>Device</td>
<td>0.65</td>
</tr>
</tbody></table>
<p><strong>Why rules instead of sentence-level extraction?</strong></p>
<p>Sentence-level relation extraction — reading the actual text between entities — requires models that are significantly slower and much harder to run at scale. At 229,498 documents on a single RTX 4060, a sentence-level model would make this stage prohibitively slow.</p>
<p>More importantly, the relation types were designed around the <em>intelligence modules</em> the product needs to power:</p>
<table>
<thead>
<tr>
<th>Intelligence Module</th>
<th>Relations that drive it</th>
</tr>
</thead>
<tbody><tr>
<td>Cross-Pollination (the matchmaker)</td>
<td><code>COMPOSED_OF</code>, <code>USED_IN</code>, <code>IMPROVES</code>, <code>DERIVED_FROM</code></td>
</tr>
<tr>
<td>Trend Velocity (early warning)</td>
<td><code>IMPROVES</code>, <code>REPLACES</code></td>
</tr>
<tr>
<td>Patent De-Risking (the shield)</td>
<td><code>DERIVED_FROM</code>, <code>SYNTHESIZED_FROM</code></td>
</tr>
<tr>
<td>Gap Analysis (opportunity finder)</td>
<td><code>TREATS</code>, <code>INHIBITS</code>, <code>CAUSES</code></td>
</tr>
</tbody></table>
<p>Relations are the product's knowledge model, not a neutral linguistic extraction. This makes the v1 rule-based approach a reasonable first pass — fast, predictable, and sufficient to validate the core product thesis.</p>
<p><strong>The known limitation:</strong> a paper stating "compound X does NOT treat disease Y" still emits a <code>TREATS</code> relation. v2 will use sentence-level extraction via SciSpacy relation models or Mistral 7B to read actual sentence text. That's a future phase.</p>
<p>All thresholds are tunable via <code>params.yaml</code>:</p>
<pre><code class="language-yaml">relations:
  confidence_threshold: 0.7
  max_relations_per_doc: 10
</code></pre>
<p><code>confidence_threshold</code> drops low-confidence rules before they hit the graph. <code>max_relations_per_doc</code> prevents combinatorial explosion on entity-dense papers.</p>
<p><strong>Results:</strong> <strong>2,294,895 relations</strong> extracted across all 12 domains.</p>
<hr />
<h2>What Each Stage Stores</h2>
<table>
<thead>
<tr>
<th>Stage</th>
<th>Tool</th>
<th>Format</th>
<th>What's stored</th>
</tr>
</thead>
<tbody><tr>
<td>Ingestion</td>
<td>DVC</td>
<td>JSON files</td>
<td>Raw paper abstracts</td>
</tr>
<tr>
<td>NER</td>
<td>DVC + MLflow</td>
<td>JSONL per domain</td>
<td>Entity mentions per paper</td>
</tr>
<tr>
<td>Relations</td>
<td>DVC + MLflow</td>
<td>JSONL per domain</td>
<td>Relation pairs per paper</td>
</tr>
</tbody></table>
<p>One important detail: after Stage 4 (graph loading), the JSONL files are no longer the source of truth. Neo4j deduplicates 10.7M raw entity mentions into 1,529,916 unique nodes via <code>MERGE</code>. The files are only needed if you want to rebuild the graph from scratch via <code>dvc repro</code>.</p>
<hr />
<h2>What's Next</h2>
<p>Stage 4 takes the JSONL output and loads it into Neo4j. But loading 10.7M entity mentions into a graph database naively takes hours — and produces a graph full of duplicates.</p>
<p>Part 3 covers the graph loader: the Neo4j schema, the <code>MERGE</code> strategy that deduplicates 10.7M mentions into 1.5M unique nodes, the batch Cypher patterns that make it tractable at scale, and the embedding pipeline that links every entity node to its Qdrant vector via an <code>embedding_id</code> property.</p>
<p><em>Follow the series on Hashnode to get Part 3 when it drops.</em></p>
<hr />
<p><em>Questions about the NLP pipeline, the relation type design, or DVC setup? Drop them in the comments — I read everything.</em></p>
]]></content:encoded></item><item><title><![CDATA[Building Insight Engine: How I'm Using GraphRAG to Discover Cross-Domain Innovation (Part 1)]]></title><description><![CDATA[A deep-dive into building a B2B SaaS knowledge discovery platform — powered by 229K research papers, a knowledge graph, and a single RTX 4060 GPU.]]></description><link>https://sumukha87.hashnode.dev/building-insight-engine-how-i-m-using-graphrag-to-discover-cross-domain-innovation-part-1</link><guid isPermaLink="true">https://sumukha87.hashnode.dev/building-insight-engine-how-i-m-using-graphrag-to-discover-cross-domain-innovation-part-1</guid><dc:creator><![CDATA[SriSumukha S]]></dc:creator><pubDate>Mon, 23 Mar 2026 14:01:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69c132db30a9b81e3a45fdd9/defdcca6-b4bb-44f7-bd64-57ad8b28df2b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This is Part 1 of an ongoing "Building in Public" series on Insight Engine — a GraphRAG-powered research discovery platform I'm actively developing. I'll be sharing architecture decisions, failures, lessons, and progress as the project evolves.</em></p>
<hr />
<h2>The Problem Nobody Has Solved Well</h2>
<p>Innovation rarely happens inside a single domain. The surgical robot borrowed from automotive manufacturing. mRNA vaccines borrowed from cancer research. Lithium-ion battery chemistry borrowed from aerospace.</p>
<p>Yet almost every research discovery tool available today — Google Scholar, Semantic Scholar, internal enterprise search — is fundamentally siloed. They help you go <em>deeper</em> into a domain. They cannot help you go <em>across</em> them.</p>
<p>This is the problem Insight Engine is built to fix.</p>
<hr />
<h2>What Insight Engine Actually Is</h2>
<p>Insight Engine is a B2B SaaS platform for cross-domain innovation discovery. Given a research question, it doesn't just retrieve similar papers — it traverses a knowledge graph to find <em>structural connections</em> between research fields that no single paper ever put together.</p>
<p>The core thesis:</p>
<blockquote>
<p>A battery-longevity breakthrough in aerospace may directly solve an unsolved problem in cardiology. But nobody ever makes the connection — because they're reading different journals, attending different conferences, and searching different databases.</p>
</blockquote>
<p>Insight Engine is the matchmaker.</p>
<hr />
<h2>The Data</h2>
<p>The platform currently operates on:</p>
<ul>
<li><strong>229,498 arXiv papers</strong> across <strong>12 research domains</strong>: Aerospace, Medical Devices, Materials, Energy, Biotechnology, Robotics, Quantum, Nanotechnology, Environment, Semiconductors, Pharma, and Neuroscience</li>
<li><strong>10,779,699 entity mentions</strong> extracted via NER (47 per document on average)</li>
<li><strong>2,294,895 relationships</strong> between entities</li>
<li><strong>1,529,916 unique entity nodes</strong> in the knowledge graph after deduplication</li>
<li><strong>1,583,613 RELATES_TO edges</strong> connecting them</li>
<li><strong>1,837,582 cross-domain edges</strong> — connections between entities from different domains</li>
</ul>
<p>This is a live, growing dataset. New domains and data sources are being added as the project progresses.</p>
<hr />
<h2>The Tech Stack</h2>
<p>Every tool was chosen for a specific reason, and all of it runs locally on a single RTX 4060 8GB GPU — zero cloud cost during development.</p>
<p><strong>Data &amp; NLP</strong></p>
<ul>
<li>spaCy 3.7 + SciSpacy <code>en_core_sci_lg</code> — biomedical NER for entity extraction</li>
<li>DVC 3.x — Git-like versioning for the full data pipeline</li>
<li>MLflow 2.x — experiment tracking across every pipeline run</li>
<li>Apache Airflow 2.9 — DAG-based pipeline scheduling</li>
</ul>
<p><strong>Graph &amp; Vector</strong></p>
<ul>
<li>Neo4j Community 5.x — knowledge graph with full Cypher query support</li>
<li>Qdrant 1.9 — vector store for semantic entity search</li>
<li>nomic-embed-text v1.5 — 768-dim embeddings, runs via Ollama</li>
<li>LlamaIndex 0.10 — GraphRAG query layer connecting Neo4j and Qdrant</li>
</ul>
<p><strong>LLM</strong></p>
<ul>
<li>Mistral 7B (Q4_K_M) via Ollama — fits in 4.1GB VRAM, primary synthesis model</li>
<li>Llama 3.1 8B — fallback model for more complex reasoning</li>
</ul>
<p><strong>Backend</strong></p>
<ul>
<li>FastAPI 0.111 + SQLAlchemy 2.0 async — API layer with JWT auth</li>
<li>PostgreSQL 16 — user, auth, and session storage</li>
<li>Celery + Redis — background task processing</li>
</ul>
<p><strong>Frontend</strong></p>
<ul>
<li>Next.js 14 + TypeScript + Tailwind + shadcn/ui — dashboard</li>
<li>Sigma.js 3.x — WebGL graph visualisation handling 100K+ nodes in-browser</li>
<li>TanStack Query 5.x — server state management</li>
</ul>
<p><strong>Infrastructure</strong></p>
<ul>
<li>Docker Compose — all 13 services orchestrated in a single file</li>
<li>GitHub Actions — CI/CD with automated testing and container publishing to ghcr.io</li>
<li>Prometheus + Grafana — metrics and monitoring</li>
</ul>
<hr />
<h2>Where the Project Stands</h2>
<p>Phase 3 is active. Here's what's complete:</p>
<p>✅ Full data ingestion pipeline — 229,498 papers fetched across 12 domains<br />✅ NER pipeline — 10.7M entities extracted, all 4 DVC pipeline stages reproducible<br />✅ Knowledge graph loaded — 1.5M entity nodes, 1.6M edges in Neo4j<br />✅ Embedding pipeline — 1,529,916 entities embedded and stored in Qdrant<br />✅ GraphRAG query engine — first cross-domain query working end-to-end<br />✅ FastAPI backend — JWT auth, refresh token rotation, async endpoints<br />✅ Next.js dashboard — 5 tabs, Sigma.js graph visualisation<br />✅ CI/CD — GitHub Actions running tests on every PR  </p>
<p>GraphRAG query latency currently sits at ~32 seconds (p95): 1s embedding, 0.2s Qdrant, 12s Neo4j traversal, 14s Mistral synthesis. Optimisation is ongoing.</p>
<hr />
<h2>What Makes This Different from Vanilla RAG</h2>
<p>Standard vector RAG — what most tools do — finds things that are <em>already known to be related</em> because they co-occur in text and are therefore close in embedding space.</p>
<p>Graph traversal finds things that are <em>structurally connected through chains of evidence</em> that no single document ever assembled.</p>
<p>When you ask <em>"aerospace materials for cardiac implants"</em>, a vector search returns aerospace papers and cardiac papers ranked by similarity. The connection between a titanium alloy fatigue resistance paper and an aortic stenosis treatment paper — a connection that spans a 3-hop graph path — is invisible to pure vector search.</p>
<p>Part 3 will cover how Neo4j and Qdrant work together to solve this. For now: Qdrant gets you to the right door, Neo4j takes you through it.</p>
<hr />
<h2>Why Build in Public?</h2>
<p>Two reasons.</p>
<p>First — writing forces clarity that coding alone doesn't. If I can't explain a decision, I probably don't understand it well enough.</p>
<p>Second — most build-in-public content shows polished results. I want to show the process: the architectural decisions, the dead ends, the production bugs, the tradeoffs. There have been plenty already — DVC broken by an fsspec version conflict, an embedding pipeline pagination bug that silently skipped entities, a Neo4j MERGE key design that had to be rethought mid-load. All of it will be documented here.</p>
<hr />
<h2>What's Coming in Part 2</h2>
<p>Next post covers the NLP pipeline in detail — how spaCy and SciSpacy extract 10.7 million entities from scientific text, the 12 entity types and why they were chosen, and how 2.3 million relationships are assigned using a rule-based system designed around the intelligence modules the product needs to power.</p>
<p><em>Follow the series on Hashnode to get Part 2 when it drops.</em></p>
<hr />
<p>*Questions about the architecture, stack choices, or anything else? Drop them in the comments</p>
]]></content:encoded></item></channel></rss>