Skip to content

feat(blog): add SQLite FTS5 + Dense hybrid retrieval article - #251

Open
ishwar170695 wants to merge 3 commits into
kubesimplify:mainfrom
ishwar170695:post/sqlite-fts5-dense-hybrid-retrieval
Open

feat(blog): add SQLite FTS5 + Dense hybrid retrieval article#251
ishwar170695 wants to merge 3 commits into
kubesimplify:mainfrom
ishwar170695:post/sqlite-fts5-dense-hybrid-retrieval

Conversation

@ishwar170695

@ishwar170695 ishwar170695 commented Jul 11, 2026

Copy link
Copy Markdown

Changes proposed

Adds a new, technical, practitioner-led article to the Kubesimplify blog focusing on hybrid retrieval architectures in local RAG systems.

  • New: content/blog/sqlite-fts5-dense-hybrid-retrieval.md — Deep-dive into sparse vs. dense search limitations, SQLite FTS5 BM25 configurations, Reciprocal Rank Fusion (RRF), deterministic domain reranking, and vector cache memory footprints.
  • New: public/img/blog/sqlite-fts5-dense-hybrid-retrieval/ — Static assets (architecture diagrams, benchmarks, UI screenshots).
  • Modified: content/authors.json — Add author profile entry for ishwar.
  • New: public/img/authors/ishwar.jpg — Author avatar placeholder.

No other changes are made to the site's code.

Note to reviewers

This post focuses on systems-level search engineering and database schemas (SQLite), which is well-suited for KubeSimplify's backend, cloud-native, and infrastructure audience.

@ishwar170695
ishwar170695 force-pushed the post/sqlite-fts5-dense-hybrid-retrieval branch from 919d215 to 0e21673 Compare July 11, 2026 11:46
Signed-off-by: Ishwar <ishwarcm@iitbhilai.ac.in>
@ishwar170695
ishwar170695 force-pushed the post/sqlite-fts5-dense-hybrid-retrieval branch from 0e21673 to 9e914a0 Compare July 11, 2026 11:47

@saiyam1814 saiyam1814 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together. I read through the article and the added assets. Overall, this is a strong technical draft: it has a clear retrieval problem, a concrete architecture, code snippets, benchmark numbers, screenshots, and a useful tradeoff section. I think it can fit the KubeSimplify audience well if we frame it as AI infrastructure / backend retrieval engineering rather than only as a generic RAG article.

A few changes I would suggest before publishing:

  1. Use the architecture diagram in the article
    The PR adds public/img/blog/sqlite-fts5-dense-hybrid-retrieval/architecture.png, but the post currently shows the pipeline as an ASCII diagram. This topic benefits a lot from a visual architecture diagram, so I would embed the image in the Architecture Overview section and optionally keep the text explanation around it.

  2. Fix the author image
    public/img/authors/ishwar.jpg appears to be the same file as the blog cover.jpg. That looks accidental. Please replace it with an actual author avatar or a suitable placeholder image.

  3. Add more context around the benchmark numbers
    The reported improvements are compelling (466 ms to 12 ms, 320 MB to 48 MB, 68% to 91% Top-5 retrieval), but they need enough context to be credible and reproducible. Please add details such as corpus size, hardware, Node/SQLite versions, embedding model/vector dimension, how the 100-query benchmark was constructed, and how relevance was judged.

  4. Verify math rendering for the RRF formula
    The post uses $$...$$ for the RRF equation. Please verify this renders correctly in the KubeSimplify site. If math rendering is not enabled, convert it to a plain-text/code-block formula so readers do not see raw LaTeX.

  5. Make the KubeSimplify fit more explicit
    I would add a short section on where this belongs operationally: local-first backends, edge apps, small/medium corpora, Kubernetes workloads with persistent storage, serverless constraints, and when teams should move to Qdrant/Milvus/Elasticsearch. This would connect the article more strongly with KubeSimplify’s cloud-native/infrastructure audience.

  6. Add a note on FTS5 table population/sync
    The SQLite FTS5 external-content table example is useful, but readers may wonder how sections_fts stays in sync with sections. A short ingestion snippet or note about triggers/rebuild flow would make the implementation more complete.

  7. Polish the style for KubeSimplify
    Consider removing emojis from section headings and using a cleaner tutorial/deep-dive style. The current content is good, but simpler headings would make it feel more consistent with existing KubeSimplify technical posts.

Overall: this is close and worth publishing after the above polish. The idea is useful, the implementation is practical, and the performance/tradeoff framing is exactly the kind of detail readers will appreciate once the claims and visuals are tightened up.

@ishwar170695

Copy link
Copy Markdown
Author

@saiyam1814 Thanks for the detailed review. I've addressed all the suggested changes:

  • Embedded the architecture diagram and cleaned up the section headings.
  • Replaced the incorrect author image with a custom author avatar.
  • Expanded the benchmark methodology with corpus size details (~4,900 sections), hardware specifications (AMD Ryzen 5 5600H), software versions (Node.js v22.x, SQLite v3.x), embedding model details, and target relevance metrics.
  • Replaced the LaTeX RRF equation with Markdown-friendly plain text code block formatting.
  • Added an Operational Fit section covering local-first, Kubernetes sidecar workloads, serverless endpoints, and when to scale up to distributed vector databases.
  • Documented FTS5 synchronization using SQLite trigger templates and rebuild commands.
  • Aligned schemas/snippets with the actual codebase variables and table names (laws and laws_fts instead of the illustrative sections/sections_fts placeholders).

I also verified that the site builds successfully with these changes. Thanks again for the suggestions!

@saiyam1814

Copy link
Copy Markdown
Member

Did you test this on real hardware?

@ishwar170695

Copy link
Copy Markdown
Author

Did you test this on real hardware?

Yes. I tested everything on my local machine.

@saiyam1814

Copy link
Copy Markdown
Member

for some reason I am not able to see the preview on cloud flare, let me pull in locally and check

@shkatara

Copy link
Copy Markdown
Collaborator

@saiyam1814

Copy link
Copy Markdown
Member

@shkatara can you add feedback for this blog?

@shkatara

Copy link
Copy Markdown
Collaborator

@ishwar170695

I'm writing this from my phone so keeping it short.

Architecture diagram is not understandable at all to people who are new to this. So many flows and nodes make it confusing than clearer.

Flow representation is also not clear. How does one query branch off to different endpoints to search. Where is this configured.

FTS5. / BM25. I'm guessing searching something over text so full text search. People not working with db have no idea what this is. Avoid using short forms or at least write full form after them in ()

Synchronizing the FTS5 Virtual Table: this is worded in a way someone from not a db background would fail to understand. External content table ? What is that. What is a virtual table ?

We load only the id (string) and the coordinate list—pre-processed into a compact Float32Array object—into memory: what id is this of ? What is in the float32 array

Explain the sql queries in english

Because the architecture or packet flow is not clear, I can't put a mental model of why memory usage comes down by 85%

What is RRF. What is BM25.

The problem statement is good. And one that makes sense. The answer goes into a lot of depth and jargons that makes it hard to follow. I had to try at least three times to read it and always I could not finish.

The blog should focus on keeping it simple. Keeping in mind people have no idea what we are writing about and would need help at each layer to put a mental model. If they are fighting to understand what these short forms are, the purpose is defeated.

It's better to give them ideas that something like this is possible if the topic is dense, and let them explore their own cases.

@saiyam1814

Copy link
Copy Markdown
Member

@ishwar170695 can you work on the feedback from @shkatara

@ishwar170695
ishwar170695 force-pushed the post/sqlite-fts5-dense-hybrid-retrieval branch from 018a736 to 7d7b6d0 Compare August 11, 2026 14:53
@ishwar170695

Copy link
Copy Markdown
Author

@saiyam1814 @shkatara Thanks for the feedback. I've reworked the article to make it more beginner-friendly by simplifying the diagrams, reducing jargon, introducing concepts before acronyms, and focusing on the mental model first. I'd appreciate another review when you have a chance.

Sorry for the delay, and thanks for your patience.

@saiyam1814

Copy link
Copy Markdown
Member

Thanks for the rework @ishwar170695, and no worries at all about the delay.

The clarity pass really landed. The new pipeline diagram is exactly what @shkatara was asking for: one clean top-to-bottom flow instead of the earlier tangle. Putting the mental model up front before any code was the right call, the 50 → 20 → 5 funnel is a genuinely nice way to teach candidate reduction, and the opening (asking about a forged signature and getting counterfeit coins back) is one of the better hooks we have had on the blog. The trade-offs section and the "fix retrieval before you reach for a bigger model" conclusion are the parts I would keep untouched.

I then went through the article side by side with the LawDecoder repo, and a few things have drifted apart between the post and the code. I want to close those before we publish, mainly to protect you: a post like this attracts readers who will clone the repo and run your snippets, and it is much better if everything they find matches.

Before we publish

1. The FTS5 snippet does not match init_db.js

The post (lines 109-120) shows an external content table:

CREATE VIRTUAL TABLE IF NOT EXISTS laws_fts USING fts5(
    id UNINDEXED, title, content,
    content='laws',
    content_rowid='rowid'
);

with the caption that content='laws' avoids duplicating text on disk. But backend/utils/init_db.js creates a standard FTS5 table with no content= option, over (id UNINDEXED, law_name, chapter, title, content), and the ingest transaction runs both insertLaw.run(...) and insertFts.run(...) for every section. So the text is duplicated today, and the caption claims the opposite.

Two ways to fix, either is fine:

  • Show what the code actually does, and mention external content tables as the next optimization, or
  • Switch the code to external content and show that. If you go this route you will also need the sync triggers, which ties back to my earlier point 6. That note is not in the current draft anymore, and it is worth having: a reader who copies an external content table without triggers gets a silently empty index.

Also, content_rowid='rowid' is a no-op as written, since rowid is already the default.

2. The repo link promises benchmark scripts

Line 228 says the benchmark scripts are on GitHub, but I could not find a harness in the repo, and .gitignore excludes backend/data/*, so the corpus, the vectors, and the .db do not ship either. That means a reader cannot reproduce any of the four headline numbers even if they want to. Easiest fix is to commit the script you used (even a rough one is fine and honestly makes the post stronger), or reword the line to promise only the source and the indexing pipeline and say plainly that the numbers are from your local run.

3. The reranker deserves its own honest paragraph

In generateLegalAnswer.js the domain reranker is:

const isDocumentForgeryRelated = queryLower.includes('signature') || queryLower.includes('sign') || queryLower.includes('document');
// ... coin/stamp/currency matches: adjustedScore *= 0.01
// ... titles containing forgery/forged: adjustedScore *= 3.0

Hardcoded guardrails like this are completely legitimate, plenty of production retrieval systems ship exactly this. The issue is presentation. Right now the article's hook is the one query this rule was written for, so a reader naturally credits FTS5 and RRF for turning counterfeit coins into forgery sections, when part of the credit belongs to the rule. Two options: run a quick ablation over your 100 queries (RRF only vs RRF plus reranker) and publish both numbers, or add a short paragraph saying the reranker is a deliberate domain guardrail for this query class rather than a general component. The ablation would be a great addition if you have the time, since "how much did each stage buy me" is the question every reader will have.

One small bug while you are in there: includes('sign') is a substring match, so it also fires on design, assign, resignation, and consignment.

4. The methodology details from the last round got lost

Your earlier reply mentioned the Ryzen 5 5600H, Node and SQLite versions, and the embedding model, but they are not in the current file, I think the rewrite dropped them. Worth putting back, especially the embedding model and dimension (Xenova/all-MiniLM-L6-v2, 384-dim, from cache_embedding.js), since readers will want to know whether their own model choice changes the picture. Your repo README also carries a caveat the post does not: memory is process-level heap at startup. That kind of precision helps the number rather than weakening it.

5. The memory story needs a breakdown

This is the one I got stuck on, and I think it is also what @shkatara meant when he said he could not build a mental model for the 85%. Running the arithmetic: 4,892 sections at 384 dims and 4 bytes each is roughly 7.5 MB for the entire vector cache, and the statutory text for 4,892 sections is maybe 10 to 20 MB. Neither of those explains a 270 MB drop. My guess is the heap is actually dominated by the transformers models (distilbart-cnn-12-6 is not small) plus JSON.parse overhead on the old per-query scan, rather than by document strings.

If that is right, the fix is easy and the post gets more interesting, not less: show a rough heap breakdown, and say the win came from dropping a full JSON parse and keeping typed arrays instead of object graphs. If I have the wrong end of it, a couple of numbers in the section will settle it.

6. Name the real cause of the latency win

The table labels v1 as a linear JSON scan, which is honest, but the surrounding narrative lets the reader attribute 466 ms to 12 ms to hybrid retrieval. Most of that gap is really "we stopped re-parsing a JSON file on every query" and would have shown up even without FTS5 or RRF. One sentence saying so keeps the claim solid, and the hybrid architecture still has plenty to stand on with the accuracy result.

Polish

  • Percentages: 320 MB to 50 MB is 84.4%, not 85.0%. Your README says ~48 MB, which is where 85.0% comes from, so I think the post rounded the MB and kept the old percentage. Also, 68% to 91% is +23 percentage points, a 34% relative gain, so "percentage points" is the safer wording.
  • Citation label: "Evidence Act Sec 65" should be "Bharatiya Sakshya Adhiniyam, 2023 Sec 65". BSA s.65 is proof of signature and handwriting (the old IEA s.67), whereas IEA s.65 is secondary evidence of documents, so a lawyer reading the table would be sent to the wrong provision. Worth being precise in a legal-domain post. Also, the post lists the corpus as BNS, BNSS, and BSA, while the README adds the IT Act, the Constitution, and the personal laws.
  • Screenshots: landing_page.png is an empty search box and a disclaimer, so it does not show anything working. citations_view.png is the good one, it matches the article's table, though its alt text mentions RRF ranks that are not visible in the frame. Your app has a Developer Mode toggle that does show the RRF ranks, and the Developer Notes and Benchmarks tab is where the numbers live, so screenshots of those two would be much stronger than the landing page. One small thing: the screenshot order is 340, 336, 339, 335 while the table has 336 first, worth reconciling or noting they are different runs.
  • Image sizes: diagram_pipeline.png and chart_memory.png are about 1.2 MB each. Converting to webp will cut that by 90% or more. Recent posts on the site use webp covers too, and the cover here is a 1024x682 jpg where our pattern is 1200x630.
  • Diagram tweak: two cheap additions would make it carry more weight: put the 50/50, 20, 5 counts on the arrows, and mark which boxes are RAM and which are disk. That is precisely the mental model your memory section depends on.
  • Front matter: datePublished is 2026-07-11, so bump it to the actual publish date, and add draft: false to match the other posts.
  • Where this fits for our readers: this was my earlier point 5 and it is not in the current draft. The trade-offs section covers when not to use the approach, but not why a Kubernetes-focused reader should care. A short section would connect it: retrieval as a sidecar with the .db baked into the image or mounted from a PVC, single-writer meaning read-only replicas scale fine, and the point where you would move to Qdrant or Milvus.
  • House style: our posts do not usually number the H2s, so plain headings would fit better, and we tend to avoid em dashes.
  • Title: "Beat Naive RAG" is a bit strong given points 3, 5, and 6. Something flatter, closer to "Hybrid retrieval with SQLite FTS5 and dense vectors", matches what the post actually demonstrates.

Where that leaves us

The 6 items in the first section are what I would like fixed before we merge, and 1, 2, and 3 are the important ones since they are what a reader would notice when they open the repo. The polish list is quick and mostly mechanical.

To be clear about the overall read: the idea is good, the engineering is real, and the writing is a big step up from the first draft. This is worth publishing once the post and the code tell the same story. Ping me when you have pushed and I will do another pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants