add semantic search
This commit is contained in:
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
40
src/routes/api/poetry/search/+server.ts
Normal file
40
src/routes/api/poetry/search/+server.ts
Normal 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);
|
||||
}
|
Reference in New Issue
Block a user