add semantic search

This commit is contained in:
2024-05-31 01:31:37 -04:00
parent 53635f0d59
commit de9cccabda
19 changed files with 1398 additions and 105 deletions

View File

@@ -1,5 +1,21 @@
<script>
<script lang="ts">
import '../../app.css';
import { searchResults } from '$lib/store';
import type { SearchResult } from '$lib/utils/search';
import SearchResults from '$lib/components/SearchResults.svelte';
let searchQuery = '';
async function handleSearch() {
const response = await fetch(`/api/poetry/search?q=${encodeURIComponent(searchQuery)}`);
if (response.ok) {
const data: SearchResult[] = await response.json();
searchResults.set(data);
} else {
console.error('Failed to fetch search results');
searchResults.set([]);
}
}
</script>
<div class="flex flex-col h-screen">
@@ -35,7 +51,27 @@
</div>
<a class="link-primary text-xl" href="/">silentsilas</a>
</div>
<div class="navbar-end lg:hidden">
<div class="form-control">
<input
type="text"
placeholder="Search"
class="input input-bordered md:w-auto"
bind:value={searchQuery}
on:input={handleSearch}
/>
</div>
</div>
<div class="navbar-end hidden lg:flex">
<div class="form-control">
<input
type="text"
placeholder="Search"
class="input input-bordered md:w-auto"
bind:value={searchQuery}
on:input={handleSearch}
/>
</div>
<ul class="menu menu-horizontal px-1">
<li><a href="/thoughts">Thoughts</a></li>
<li><a href="/poetry">Poetry</a></li>
@@ -48,5 +84,6 @@
<div class="flex flex-col items-center flex-1 overflow-auto">
<slot />
<SearchResults />
</div>
</div>

View File

@@ -1,6 +1,14 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { searchResults } from '$lib/store';
import type { SearchResult } from '$lib/utils/search';
let results: SearchResult[] = [];
searchResults.subscribe((value: SearchResult[]) => {
results = value ? value : [];
});
type Greeting = {
greeting: string;
@@ -59,48 +67,48 @@
}
</script>
<div class="container mx-auto flex flex-col justify-center items-center flex-1">
<div class="justify-center items-center text-center m-10">
{#if visible && currentGreeting}
<div
transition:fade={{ duration: 1200 }}
>
<span class="font-bold">{currentGreeting.greeting}</span>
{#if currentGreeting.romanisation}
<span class="text-gray-500">( {currentGreeting.romanisation} )</span>
{/if}
</div>
<p class="mt-2 text-gray-700" transition:fade={{ delay: 400, duration: 400 }}>
That's {currentGreeting.language} for hello!
{#if results.length <= 0}
<div class="container mx-auto flex flex-col justify-center items-center flex-1">
<div class="justify-center items-center text-center m-10">
{#if visible && currentGreeting}
<div transition:fade={{ duration: 1200 }}>
<span class="font-bold">{currentGreeting.greeting}</span>
{#if currentGreeting.romanisation}
<span class="text-gray-500">( {currentGreeting.romanisation} )</span>
{/if}
</div>
<p class="mt-2 text-gray-700" transition:fade={{ delay: 400, duration: 400 }}>
That's {currentGreeting.language} for hello!
</p>
{/if}
</div>
<div class="text-center prose px-4">
<p>
The name's Silas. I write code for a living, and sometimes for fun. I use <a
href="https://elixir-lang.org/"
target="_blank">Elixir</a
>
at my day job, and recently have been messing around with
<a href="https://elixir-lang.org/" target="_blank">Rust</a>,
<a href="https://kit.svelte.dev/" target="_blank">Svelte</a>, and
<a href="https://threejs.org/" target="_blank">three.js</a>
</p>
{/if}
<p>
Here you can browse my shower <a href="/thoughts" class="link">thoughts</a> and bad
<a href="/poetry" class="link">poetry</a>. Opinions are personally mine and not endorsed by
my employer.
</p>
<p>
I tend to start a lot of <a href="/projects" class="link">projects</a>, but I'm trying to
finish more. I also like to toy with weird web technologies and will host the
<a href="/experiments" class="link">experiments</a> here.
</p>
<p>
I self-host a lot of <a href="/services" class="link">services</a> I find useful. None of them
run any analytics or log your activity, but the software/servers may be outdated, so use at your
own risk.
</p>
<p>Shalom.</p>
</div>
</div>
<div class="text-center prose px-4">
<p>
The name's Silas. I write code for a living, and sometimes for fun. I use <a
href="https://elixir-lang.org/"
target="_blank">Elixir</a
>
at my day job, and recently have been messing around with
<a href="https://elixir-lang.org/" target="_blank">Rust</a>,
<a href="https://kit.svelte.dev/" target="_blank">Svelte</a>, and
<a href="https://threejs.org/" target="_blank">three.js</a>
</p>
<p>
Here you can browse my shower <a href="/thoughts" class="link">thoughts</a> and bad
<a href="/poetry" class="link">poetry</a>. Opinions are personally mine and not endorsed by my
employer.
</p>
<p>
I tend to start a lot of <a href="/projects" class="link">projects</a>, but I'm trying to
finish more. I also like to toy with weird web technologies and will host the
<a href="/experiments" class="link">experiments</a> here.
</p>
<p>
I self-host a lot of <a href="/services" class="link">services</a> I find useful. None of them
run any analytics or log your activity, but the software/servers may be outdated, so use at your
own risk.
</p>
<p>Shalom.</p>
</div>
</div>
{/if}

View File

@@ -1,9 +1,17 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { searchResults } from '$lib/store';
import type { SearchResult } from '$lib/utils/search';
import type { PageData } from '../poetry/$types';
export let data: PageData;
let results: SearchResult[] = [];
searchResults.subscribe((value: SearchResult[]) => {
results = value ? value : [];
});
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString(undefined, {
year: 'numeric',
@@ -35,36 +43,38 @@
}
</script>
<div class="container mx-auto flex flex-col items-center">
<div class="prose">
<h1 class="py-6">Poetry</h1>
</div>
{#if results.length <= 0}
<div class="container mx-auto flex flex-col items-center">
<div class="prose">
<h1 class="py-6">Poetry</h1>
</div>
<ul>
{#each posts as post}
<li class="py-4">
<h3 class="pb-1">
<a class="link" href={post.path}>
{post.meta.title}
</a>
</h3>
<p class="text-sm">{formatDate(post.meta.date)}</p>
</li>
{/each}
</ul>
</div>
{#if total > 1}
<nav class="join justify-end py-10">
<button
class="join-item btn-primary btn btn-outline"
on:click={() => navigate(currentPage - 1)}
disabled={currentPage === 1}>Prev</button
>
<button class="join-item btn btn-outline">{currentPage} of {totalPages}</button>
<button
class="join-item btn btn-primary btn-outline"
on:click={() => navigate(currentPage + 1)}
disabled={currentPage === totalPages}>Next</button
>
</nav>
<ul>
{#each posts as post}
<li class="py-4">
<h3 class="pb-1">
<a class="link" href={post.path}>
{post.meta.title}
</a>
</h3>
<p class="text-sm">{formatDate(post.meta.date)}</p>
</li>
{/each}
</ul>
</div>
{#if total > 1}
<nav class="join justify-end py-10">
<button
class="join-item btn-primary btn btn-outline"
on:click={() => navigate(currentPage - 1)}
disabled={currentPage === 1}>Prev</button
>
<button class="join-item btn btn-outline">{currentPage} of {totalPages}</button>
<button
class="join-item btn btn-primary btn-outline"
on:click={() => navigate(currentPage + 1)}
disabled={currentPage === totalPages}>Next</button
>
</nav>
{/if}
{/if}

View File

@@ -0,0 +1,40 @@
// eslint-disable-next-line
import * as tf from '@tensorflow/tfjs-node';
import poemEmbeddings from '$lib/utils/poetry/embeddings.json';
import { json } from '@sveltejs/kit';
import { getModel, type Embedding, type SearchResult } from '$lib/utils/search';
// Search handler
export const GET = async ({ url }: { url: URL }) => {
const model = await getModel();
const searchQuery = url.searchParams.get('q');
if (!searchQuery) {
return { status: 400, body: { error: 'Query parameter "q" is required' } };
}
try {
// Generate embedding for the query
const queryEmbedding = await model.embed([searchQuery]);
const queryVec = queryEmbedding.arraySync()[0];
// Calculate similarities
const results = poemEmbeddings
.map((poem: Embedding) => ({
poem,
similarity: cosineSimilarity(queryVec, poem.vector)
}))
.sort((a: SearchResult, b: SearchResult) => b.similarity - a.similarity)
.slice(0, 10); // Top 10 results
return json(results);
} catch (error) {
return { status: 500, body: { error: (error as Error).message } };
}
};
function cosineSimilarity(vecA: number[], vecB: number[]) {
const dotProduct = vecA.reduce((acc, val, i) => acc + val * vecB[i], 0);
const magnitudeA = Math.sqrt(vecA.reduce((acc, val) => acc + val * val, 0));
const magnitudeB = Math.sqrt(vecB.reduce((acc, val) => acc + val * val, 0));
return dotProduct / (magnitudeA * magnitudeB);
}