From aeefd4a15736b5258ec32ceb5ee0a9b7809df620 Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Thu, 12 Mar 2026 08:16:41 +0100 Subject: [PATCH 1/3] allow for deleting all tags on a snippet --- bruno/CodeSnippets/delete_snippet.bru | 2 +- .../CodeSnippets/{get_snippet_by_slug.bru => get_snippet.bru} | 4 ++-- bruno/CodeSnippets/ping.bru | 2 +- src/snippets/infra/db.clj | 4 +++- src/snippets/use_cases/view.clj | 4 +++- 5 files changed, 10 insertions(+), 6 deletions(-) rename bruno/CodeSnippets/{get_snippet_by_slug.bru => get_snippet.bru} (87%) diff --git a/bruno/CodeSnippets/delete_snippet.bru b/bruno/CodeSnippets/delete_snippet.bru index 9311985..b3eb046 100644 --- a/bruno/CodeSnippets/delete_snippet.bru +++ b/bruno/CodeSnippets/delete_snippet.bru @@ -1,7 +1,7 @@ meta { name: delete_snippet type: http - seq: 5 + seq: 6 } delete { diff --git a/bruno/CodeSnippets/get_snippet_by_slug.bru b/bruno/CodeSnippets/get_snippet.bru similarity index 87% rename from bruno/CodeSnippets/get_snippet_by_slug.bru rename to bruno/CodeSnippets/get_snippet.bru index 3bea0bb..6115b49 100644 --- a/bruno/CodeSnippets/get_snippet_by_slug.bru +++ b/bruno/CodeSnippets/get_snippet.bru @@ -1,7 +1,7 @@ meta { - name: get_snippet_by_slug + name: get_snippet type: http - seq: 10 + seq: 5 } get { diff --git a/bruno/CodeSnippets/ping.bru b/bruno/CodeSnippets/ping.bru index 7bc3b14..2a90a2d 100644 --- a/bruno/CodeSnippets/ping.bru +++ b/bruno/CodeSnippets/ping.bru @@ -1,7 +1,7 @@ meta { name: ping type: http - seq: 1 + seq: 2 } get { diff --git a/src/snippets/infra/db.clj b/src/snippets/infra/db.clj index 260fbae..a0dde8e 100644 --- a/src/snippets/infra/db.clj +++ b/src/snippets/infra/db.clj @@ -163,7 +163,9 @@ new-tags (get raw-patch :tags '[]) existing-tags (get snippet :snippet/tags '[]) tags-to-remove (vec (set/difference (set existing-tags) (set new-tags))) - retracts (map #(vector :db/retract eid :snippet/tags %) tags-to-remove) + retracts (if (nil? (:tags raw-patch)) + nil + (map #(vector :db/retract eid :snippet/tags %) tags-to-remove)) patch (merge (to-update raw-patch) {:db/id eid})] (t/log! {:level :info, :data {:patch patch :retracts retracts :slug slug :eid eid}} "Patching snippet") (when (nil? eid) diff --git a/src/snippets/use_cases/view.clj b/src/snippets/use_cases/view.clj index 26f8d6c..dc0774c 100644 --- a/src/snippets/use_cases/view.clj +++ b/src/snippets/use_cases/view.clj @@ -7,7 +7,9 @@ "Converts snippet pub-date to ISO-8601 string for EDN serialization" [snippet] (when snippet - (assoc snippet :pub-date (.toString (:pub-date snippet))))) + (-> snippet + (assoc :tags (if (nil? (:tags snippet)) '[] (:tags snippet))) + (assoc :pub-date (.toString (:pub-date snippet)))))) (defn view-snippets [options] (let [limit (:limit options) From c7bca62df4984260a7de0352ad8eb223cb93bf4d Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Thu, 12 Mar 2026 09:24:39 +0100 Subject: [PATCH 2/3] create text embeds on create and updsate --- src/snippets/infra/text_embed.clj | 13 ++++++++++--- src/snippets/use_cases/create.clj | 4 ++-- src/snippets/use_cases/edit.clj | 2 ++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/snippets/infra/text_embed.clj b/src/snippets/infra/text_embed.clj index 6be3143..17f8e40 100644 --- a/src/snippets/infra/text_embed.clj +++ b/src/snippets/infra/text_embed.clj @@ -54,11 +54,18 @@ "Save an embedding to Qdrant" [snippet embed] (let [api-key (:qdrant-api-key (config)) - id (db/slug-to-db-id (:slug snippet))] - (t/log! {:level :info :data {:slug (:slug snippet) :api-key api-key :id id}} "Saving embedding for snippet") - (http/put (str (:qdrant-host (config)) "/collections/snippets-dev/points") + id (db/slug-to-db-id (:slug snippet)) + host (str (:qdrant-host (config)) "/collections/snippets-dev/points")] + (t/log! {:level :info :data {:slug (:slug snippet) :id id :host host}} "Saving embedding for snippet") + (http/put host {:headers {"api-key" api-key} :content-type :json :form-params {:points [{:id id :vector embed :payload {:slug (:slug snippet)}}]} ;; :cookie-store false :as :json}))) + +(defn get-and-save-embed + "Get an embedding for a snippet and save it to Qdrant" + [snippet] + (let [embed (get-embed snippet)] + (save-embed snippet embed))) diff --git a/src/snippets/use_cases/create.clj b/src/snippets/use_cases/create.clj index 33a41ba..35d5ca9 100644 --- a/src/snippets/use_cases/create.clj +++ b/src/snippets/use_cases/create.clj @@ -1,11 +1,11 @@ (ns snippets.use-cases.create (:require [taoensso.telemere :as t] + [snippets.infra.text-embed :as embed] [snippets.infra.db :as db])) (defn create-snippet [{:keys [title slug markdown tags]}] (let [pub-date (java.util.Date.)] (t/log! {:level :info, :data {:title title :slug slug}} "Creating snippet") (db/create-snippets [{:title title :slug slug :markdown markdown :tags tags :pub-date pub-date}]) - ;; TODO: caculate text embed vector - )) + (embed/get-and-save-embed (db/get-snippet-by-slug slug)))) diff --git a/src/snippets/use_cases/edit.clj b/src/snippets/use_cases/edit.clj index 508f88c..838e026 100644 --- a/src/snippets/use_cases/edit.clj +++ b/src/snippets/use_cases/edit.clj @@ -1,6 +1,7 @@ (ns snippets.use-cases.edit (:require [taoensso.telemere :as t] + [snippets.infra.text-embed :as embed] [malli.core :as m] [snippets.infra.db :as db])) @@ -18,5 +19,6 @@ (do (t/log! {:level :info, :data {:patch patch :slug slug}} "Valid changes editing snippet") (db/update-snippet slug patch) + (embed/get-and-save-embed (db/get-snippet-by-slug slug)) {:success true}) {:success false :reason :invalid-patch})) From 2393faf9d00b4ad56a0aa7d568170d39a4cd4054 Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Thu, 12 Mar 2026 09:29:09 +0100 Subject: [PATCH 3/3] add similar search to api does a cool thing", --- .../CodeSnippets/{get_snippet.bru => get.bru} | 2 +- .../{get_snippets.bru => list.bru} | 2 +- bruno/CodeSnippets/similar.bru | 23 +++++++++++++++++++ src/snippets/infra/api.clj | 10 ++++++++ src/snippets/infra/db.clj | 11 +++++++++ src/snippets/infra/text_embed.clj | 18 +++++++++++++++ src/snippets/use_cases/similar_search.clj | 10 ++++++++ 7 files changed, 74 insertions(+), 2 deletions(-) rename bruno/CodeSnippets/{get_snippet.bru => get.bru} (93%) rename bruno/CodeSnippets/{get_snippets.bru => list.bru} (93%) create mode 100644 bruno/CodeSnippets/similar.bru create mode 100644 src/snippets/use_cases/similar_search.clj diff --git a/bruno/CodeSnippets/get_snippet.bru b/bruno/CodeSnippets/get.bru similarity index 93% rename from bruno/CodeSnippets/get_snippet.bru rename to bruno/CodeSnippets/get.bru index 6115b49..fb9ed8b 100644 --- a/bruno/CodeSnippets/get_snippet.bru +++ b/bruno/CodeSnippets/get.bru @@ -1,5 +1,5 @@ meta { - name: get_snippet + name: get type: http seq: 5 } diff --git a/bruno/CodeSnippets/get_snippets.bru b/bruno/CodeSnippets/list.bru similarity index 93% rename from bruno/CodeSnippets/get_snippets.bru rename to bruno/CodeSnippets/list.bru index a04a48a..9728add 100644 --- a/bruno/CodeSnippets/get_snippets.bru +++ b/bruno/CodeSnippets/list.bru @@ -1,5 +1,5 @@ meta { - name: get_snippets + name: list type: http seq: 4 } diff --git a/bruno/CodeSnippets/similar.bru b/bruno/CodeSnippets/similar.bru new file mode 100644 index 0000000..0e11126 --- /dev/null +++ b/bruno/CodeSnippets/similar.bru @@ -0,0 +1,23 @@ +meta { + name: similar + type: http + seq: 10 +} + +get { + url: {{host}}/api/similar?slug=rg-output + body: none + auth: none +} + +params:query { + slug: rg-output +} + +body:json { + { + "title": "Test Snippet", + "markdown": "## Cool Snippet\ndoes a cool thing", + "tags": ["git", "jj"] + } +} diff --git a/src/snippets/infra/api.clj b/src/snippets/infra/api.clj index 1e2622a..9da5f33 100644 --- a/src/snippets/infra/api.clj +++ b/src/snippets/infra/api.clj @@ -6,6 +6,7 @@ [snippets.use-cases.view] [snippets.use-cases.delete] [snippets.use-cases.create] + [snippets.use-cases.similar-search] [snippets.use-cases.edit] [snippets.infra.config :as config] [muuntaja.middleware :as mm] @@ -60,6 +61,14 @@ {:status 200 :body (snippets.use-cases.view/view-snippets-by-tag tag)})) +(defn handle-view-similar-snippets [{params :query-params}] + (let [slug (get params "slug")] + (cond + (nil? slug) {:status 400 + :body "Slug parameter is required"} + :else {:status 200 + :body (snippets.use-cases.similar-search/search-similar slug)}))) + (defn handle-view-snippet-by-slug [{params :query-params}] (let [slug (get params "slug") snippet (snippets.use-cases.view/view-snippet-by-slug slug)] @@ -83,6 +92,7 @@ ["/tags" {:get handle-view-tags}] ["/tag" {:get handle-view-snippets-by-tag}] ["/snippet-by-slug" {:get handle-view-snippet-by-slug}] + ["/similar" {:get handle-view-similar-snippets}] ["/snippets" {:get handle-view-snippets}] ["/snippet" {:post handle-create-snippet :patch handle-edit-snippet diff --git a/src/snippets/infra/db.clj b/src/snippets/infra/db.clj index a0dde8e..9d3b45d 100644 --- a/src/snippets/infra/db.clj +++ b/src/snippets/infra/db.clj @@ -122,6 +122,17 @@ :where [?e :snippet/slug ?slug]]] (ffirst (d/q query db slug)))) +(defn- get-by-db-id-from-db + "Get a snippet by db id" + [id] + (let [conn (get-conn) + db (d/db conn) + entity (d/pull db '[* :snippet] id)] + (if (= (keys entity) [:db/id]) nil entity))) + +(def get-by-db-id + (wrap-snippet-return get-by-db-id-from-db)) + (defn- get-snippet-by-slug-from-db "Get a single snippet by its slug." [slug] diff --git a/src/snippets/infra/text_embed.clj b/src/snippets/infra/text_embed.clj index 17f8e40..9024019 100644 --- a/src/snippets/infra/text_embed.clj +++ b/src/snippets/infra/text_embed.clj @@ -69,3 +69,21 @@ [snippet] (let [embed (get-embed snippet)] (save-embed snippet embed))) + +(defn search + "Search for similar snippet in Qdrant + returns example: [{:id 101155069755600, :version 204, :score 0.85372585}] or []" + [slug] + (let [db-id (db/slug-to-db-id slug) + api-key (:qdrant-api-key (config)) + host (str (:qdrant-host (config)) "/collections/snippets-dev/points/query")] + (t/log! {:level :info :data {:slug slug :db-id db-id}} "Searching Qdrant for similar snippets") + (when (nil? db-id) + (throw (ex-info "Invalid slug" {:slug slug}))) + (-> + (http/post host + {:headers {"api-key" api-key} + :content-type :json + :form-params {:query db-id :limit 3 :score_threshold 0.7} + :as :json}) + (get-in [:body :result :points] '[])))) diff --git a/src/snippets/use_cases/similar_search.clj b/src/snippets/use_cases/similar_search.clj new file mode 100644 index 0000000..2fd4f6d --- /dev/null +++ b/src/snippets/use_cases/similar_search.clj @@ -0,0 +1,10 @@ +(ns snippets.use-cases.similar-search + (:require [snippets.infra.text-embed :as embed] + [taoensso.telemere :as t] + [snippets.infra.db :as db])) + +(defn search-similar + [slug] + (t/log! {:level :info :data {:slug slug}} "Making a similar search by slug") + (->> (embed/search slug) + (map #(hash-map :snippet (db/get-by-db-id (:id %)), :score (:score %)))))