Originally published on iHateReading
Hello and welcome to the new blog
Search on website is what we will discuss today
Well one thing to start is not iHateReading have 300+ almost 400 blogs and in few days I’ll make it to 500 blogs
Blogs are ranges from Backend, Frontend, Node, HTML, CSS, Web3, SAAS ideas and more
Moving ahead giving search to these blogs is quite tricky not every keyboard press needs to make API call to backend server, some prefer using dbounce, onKeyboardDown keys events, delay in API call, caching the search results with keys All these methods will certainly help to add optmised and low cost search engine.
But for iHateReading, we are doing things in bit differently. In a few cases, I’ve used already existing packages like fuse.js to have a dummy but accurate search, a simple in-built custom search algorithm and third, I’ve used the client-vector-search npm module to generate vector embeddings and then add search using cosine similarity to have vector search within the website.
Let’s break it down, client-vector-search, the npm module that
- Generate embeddings
- Index embeddings
- CRUD operations on embeddings This simple npm module helps me to generate vector embeddings, an array of vectors generated from simple plain text or words. Then find the searched query related vectors to return the top matching items in the search engine.
npm i client-vector-search
Once installed, let’s move to React code where allThread are the blogs posts fetched from the database
import { getEmbedding } from "client-vector-search";
async function buildEmbeddingObjects(blogPosts) {
const objectsWithEmbeddings = [];
for (const post of blogPosts) {
const content = `${post.title}\n${post.description}`;
const embedding = await getEmbedding(content); // OpenAI under the hood
objectsWithEmbeddings.push({
id: post.id,
name: post.title, // Optional label
embedding,
post, // Store original post for retrieval later
});
}
return objectsWithEmbeddings;
}
The above method will create a vector array from the blogPosts and return the array of arrays containing the embeddings.
Then we just have to define the cosine method to find similarity.
const calculateCosineSimilarity = (vecA, vecB) => {
if (!vecA || !vecB || vecA.length !== vecB.length) return 0;
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < vecA.length; i++) {
dotProduct += vecA[i] * vecB[i];
normA += vecA[i] * vecA[i];
normB += vecB[i] * vecB[i];
}
normA = Math.sqrt(normA);
normB = Math.sqrt(normB);
if (normA === 0 || normB === 0) return 0;
return dotProduct / (normA * normB);
};
This method is one of the vector search, similar to keyword searching algorithms. Consine is the most used method.
const searchWithEmbeddings = useCallback(async (query, topK = 20) => {
if (!query.trim()) return [];
try {
const embeddingObjects = await buildEmbeddingObjects(allThreads);
const queryEmbedding = await getEmbedding(query);
const results = embeddingObjects.map((obj) => {
const similarity = calculateCosineSimilarity(
queryEmbedding,
obj.embedding
);
return {
...obj,
similarity,
item: obj.post, // Maintain compatibility with existing search structure
};
});
// Sort by similarity (highest first) and return top K results
return results
.sort((a, b) => b.similarity - a.similarity)
.slice(0, topK)
.filter((result) => result.similarity > 0.3); // Filter out low similarity results
} catch (error) {
console.error("Error in vector search:", error);
return [];
}
In the above method, we are simply using both
- buildVectorEmbeddings method to create embeddings
- consine similarity to find the similarities and return topK using the query Hence, in the end, we have the basic small vector embedding search on the client side.
Another way is using Fuse.js, I love it, but it’s not real backend work, so using Fuse.js in production is still the question.
I am using Fuse.js to find similar searches on the scraped blogs. On the iHateReading Explore page, we are scraping 300+ websites/links important for developers to find the latest and important blogs for ourselves as well as for others to read. You can find those resources on iHateReading Universo
Now, once the scraping is done, we are storing the blogs on the backend, only the unique ones, and then fetching them on the client side to add search functionality using fuse.js.
Once fetched, Fuse.js works well and is quite easy to add
npm i fuse.js
import Fuse from "fuse.js";
import {useCallback} from "react"
const [query, setQuery] = useState("");
const searchScrapedBlogs = useCallback((query, blogs) => {
if (!query.trim() || !blogs || blogs.length === 0) return [];
const fuseOptions = {
keys: ["title", "description", "link"],
threshold: 0.3,
includeScore: true,
includeMatches: true,
};
const fuse = new Fuse(blogs, fuseOptions);
const results = fuse.search(query);
return results.map((result) => ({
item: result.item,
score: result.score,
matches: result.matches,
}));
}, []);
Read more about options
In the above code, we create a fuse instance by passing an array to perform a search along with fuse options. Fuse options support threshold, include matches and others by the word.
Then we are using the same method to add search to all our newsletter emails that we have sent to our clients.
// Search newsletters using Fuse.js
const searchNewsletters = useCallback((query, emails) => {
if (!query.trim() || !emails || emails.length === 0) return [];
const fuseOptions = {
keys: ["subject"],
threshold: 0.3,
includeScore: true,
includeMatches: true,
};
const fuse = new Fuse(emails, fuseOptions);
const results = fuse.search(query);
return results.map((result) => ({
item: result.item,
score: result.score,
matches: result.matches,
}));
}, []);
One can easily understand how quickly we can add a basic dummy search for the time being to avoid API calls, backend server setup.
But if you are interested, you can use hono.js with a React app to add backend backend-related server endpoint.
The next part, which I’ll not add an LLM API layer to vector embeddings and fuse search results and pass the query with objects to the LLM to generate the response.
In this way, we can have a smart AI Assistant and Search. The idea emerges from Inkeep while reading the docs. More to cover in the next blog for today that would be enough
Shrey
Top comments (0)