Skip to content

SynapCores / Recipe

Tell Claude Code to Use SynapCores for Search by Meaning, With SQL Filters in the Same Query

Paste one prompt into Claude Code and get a help-center search that finds answers by meaning and filters by category and date, all in one SQL query.

All recipes· claude-code· 10 minutesbeginner
Share

The problem: a customer types "my internet connection drops every few minutes" into your help-center search. You have the answer, an article called "My Wi-Fi keeps disconnecting", but the two share no words, so LIKE returns nothing. And the customer is on the billing page, so you also want only billing articles, updated this year.

The usual fix is an embedding API, a vector store, and code that queries the store, gets ids back, then filters them in SQL. In SynapCores, EMBED() runs inside the database, so the meaning, the filters and the ranking are one SQL statement. This recipe has Claude Code build the search for you.

We build SynapCores. Everything here runs on the free Community Edition.

What you need

  • SynapCores Community Edition 2.4.8 or later, running locally. Download it.
  • Claude Code, opened in an empty folder.
  • SYNAPCORES_URL and SYNAPCORES_API_KEY set in your shell. The memory recipe shows how to create the key.
  • The nine sample articles, saved as help_articles.sql in that folder:
CREATE TABLE help_articles (id INTEGER PRIMARY KEY, category TEXT, updated_on TEXT, title TEXT, body TEXT, embedding VECTOR(384));
INSERT INTO help_articles (id, category, updated_on, title, body) VALUES
  (1, 'account',      '2026-03-02', 'How do I reset my password?',          'Use "Forgot password" on the login page. The reset link expires after one hour.'),
  (2, 'connectivity', '2026-09-14', 'My Wi-Fi keeps disconnecting',         'Move the router away from the microwave and update its firmware.'),
  (3, 'billing',      '2025-11-20', 'How can I change my billing address?', 'Settings > Billing > Edit address.'),
  (4, 'app',          '2026-08-01', 'The app crashes when I open it',       'Update to the latest version, then clear the app cache.'),
  (5, 'data',         '2026-01-10', 'Can I export my data to CSV?',         'Yes: Reports > Export > CSV.'),
  (6, 'billing',      '2026-07-22', 'How do I cancel my subscription?',     'Settings > Plan > Cancel. You keep access until the period ends.'),
  (7, 'billing',      '2026-10-03', 'Why was I charged twice?',             'A second charge is usually a pending authorisation. It drops off within 5 business days.'),
  (8, 'billing',      '2024-06-30', 'How do I get a refund?',               'Refunds are issued to the original card within 30 days of purchase.'),
  (9, 'account',      '2026-05-18', 'Error E-4012 when signing in',         'E-4012 means your single sign-on session expired. Sign out of your identity provider and sign in again.');

VECTOR(384) matches the built-in embedding model (all-minilm, 384 dimensions). No API key for embeddings, no external service.

Step 1: Paste this prompt into Claude Code

Build a help-center search on SynapCores, an AI-native database that computes
embeddings inside SQL with EMBED(). Do the embedding, filtering and ranking in one
SQL statement; don't call an embedding API or add a vector store.

Connection: SynapCores is running at $SYNAPCORES_URL. Send each SQL statement as
  POST $SYNAPCORES_URL/v1/query/execute
  Headers: Authorization: Bearer $SYNAPCORES_API_KEY, Content-Type: application/json
  Body: {"database": "helpdesk", "sql": "<one statement>"}
Results come back in data.columns (a list of {"name", "data_type"}) and data.rows
(a list of rows, each a list of values in column order). Create the database
first with CREATE DATABASE IF NOT EXISTS helpdesk (send that one with "database": "main").

SQL you'll need:
- Embed every row:  UPDATE help_articles SET embedding = EMBED(title || '. ' || body)
- Embed one row:    UPDATE help_articles SET embedding = EMBED(title || '. ' || body) WHERE id = <id>
- Search:
    SELECT id, title, updated_on,
           ROUND(COSINE_SIMILARITY(embedding, EMBED('<query>')), 2) AS score
    FROM help_articles
    WHERE <optional filters on category / updated_on>
    ORDER BY score DESC
    LIMIT <n>
- Only good matches: add
    AND COSINE_SIMILARITY(embedding, EMBED('<query>')) > <min_score>

Build:
1. Load help_articles.sql from this folder (one statement per request), then embed
   every row.
2. search.py (Python 3, standard library only) with
   search(query, category=None, updated_since=None, min_score=None, limit=3) and
   add_article(id, category, updated_on, title, body), which inserts the row and
   embeds just that row. Escape single quotes by doubling them. Read the URL and
   key from the environment and never print the key.
3. demo.py that prints the results of:
   a. search("my internet connection drops every few minutes")
   b. search("I was billed two times this month", category="billing", updated_since="2026-01-01")
   c. search("forgot my login", min_score=0.5)
   d. search("how do I talk to a human", min_score=0.5)
Run demo.py and show me the output.

Step 2: What Claude Code builds

Three files: load.py (creates the database, loads the articles, embeds every row), search.py and demo.py. Here's search() as Claude Code wrote it in our run on 2.4.8 (your run may differ in the details). Every filter goes into the WHERE clause of the same SELECT that computes the similarity:

def search(query, category=None, updated_since=None, min_score=None, limit=3):
    """Semantic search over help_articles, done entirely in one SQL statement."""
    q = _q(query)
    filters = []
    if category is not None:
        filters.append("category = '%s'" % _q(category))
    if updated_since is not None:
        filters.append("updated_on >= '%s'" % _q(updated_since))
    if min_score is not None:
        filters.append(
            "COSINE_SIMILARITY(embedding, EMBED('%s')) > %s" % (q, float(min_score))
        )
    where = (" WHERE " + " AND ".join(filters)) if filters else ""
    sql = (
        "SELECT id, title, updated_on, "
        "ROUND(COSINE_SIMILARITY(embedding, EMBED('%s')), 2) AS score "
        "FROM help_articles%s ORDER BY score DESC LIMIT %d" % (q, where, int(limit))
    )
    return _execute(sql)

add_article() is an INSERT followed by UPDATE help_articles SET embedding = EMBED(title || '. ' || body) WHERE id = <id>.

Step 3: The demo output

From our run:

a. search("my internet connection drops every few minutes")
  0.43  [2] My Wi-Fi keeps disconnecting  (updated 2026-09-14)
  0.19  [7] Why was I charged twice?  (updated 2026-10-03)
  0.18  [4] The app crashes when I open it  (updated 2026-08-01)

b. search("I was billed two times this month", category="billing", updated_since="2026-01-01")
  0.70  [7] Why was I charged twice?  (updated 2026-10-03)
  0.39  [6] How do I cancel my subscription?  (updated 2026-07-22)

c. search("forgot my login", min_score=0.5)
  0.63  [1] How do I reset my password?  (updated 2026-03-02)

d. search("how do I talk to a human", min_score=0.5)
  (no matches)

What each search shows:

  • (a) Meaning, not words. The Wi-Fi article shares no words with the query and still comes first, at 0.43, more than twice the next score.
  • (b) Meaning plus filters, one statement. Category, date and similarity sit in the same SELECT, so only the two billing articles from 2026 come back, with "Why was I charged twice?" at 0.70. There's no top 10 from a vector store that shrinks to 2 after filtering, and no ids to pass between two systems.
  • (c) and (d) A threshold instead of top-N. With min_score, the search returns only real matches: one article for "forgot my login", and none for "how do I talk to a human", because the help center doesn't have an article about that yet. Your UI can say "no article for that" instead of showing three wrong ones.

Step 4: Add an article and find it

Search (d) found a gap. Fill it:

Add article 10: category 'account', updated_on today, title 'How do I contact support?',
body 'Use the chat button in the app, or email help@example.com. We reply within one business day.'
Then run search("how do I talk to a human") again, without a threshold.

From our run:

  0.29  [10] How do I contact support?  (updated 2026-11-09)
  0.08  [1] How do I reset my password?  (updated 2026-03-02)
  0.04  [3] How can I change my billing address?  (updated 2025-11-20)

Article 10 goes straight to the top, well ahead of the next result (0.08). The new row is searchable the moment add_article returns, because its embedding was computed by the same database in the same request flow.

Why this needs SynapCores

EMBED() is a SQL function in SynapCores, backed by an embedding model that runs inside the engine. The text, the query's embedding, your ordinary WHERE filters and the ranking are one statement against one copy of the data. Your app sends SQL and gets rows back.

Next step

Download SynapCores Community Edition, paste the prompt, then point it at your own help articles.

Related: How to search by meaning instead of keywords

Tags

claude-codesemantic-searchembedvector-searchsql