feat(frontend): Add advanced block search with relevance ranking (#9711)

- fix #9425 

- Enhancing the functionality of searching blocks on the build page

Currently, it only performs exact matching on the block name and
description. I added a scoring mechanism for searching.

- The scoring algorithm works as follows:
     - Returns 1 if no query (all blocks match equally)
     - Normalized query for case-insensitive matching
- Returns 3 for exact substring matches in block name (highest priority)
- Returns 2 when all query words appear in the block name (regardless of
order)
- Returns 1.X for blocks with names similar to query using Jaro-Winkler
distance (X is similarity score)
- Returns 0.5 when all query words appear in the block description
(lowest priority)
     - Returns 0 for no match

Higher scores will appear first in search results.

> I have used an external library for Jaro-Winkler distance -
[link](https://www.npmjs.com/package/jaro-winkler)

Before
![Screenshot 2025-03-28 at 12 09
24 PM](https://github.com/user-attachments/assets/e135c007-cd9a-4692-88fc-3ad42b097c22)

After
![Screenshot 2025-03-28 at 12 09
17 PM](https://github.com/user-attachments/assets/28cd01c1-0d8e-44fa-8e04-ba9796118ba3)
This commit is contained in:
Abhimanyu Yadav
2025-04-07 14:24:00 +05:30
committed by GitHub
parent 73d43312d1
commit 8b2265c996
3 changed files with 74 additions and 9 deletions

View File

@@ -48,6 +48,7 @@
"@supabase/ssr": "^0.5.2", "@supabase/ssr": "^0.5.2",
"@supabase/supabase-js": "^2.49.1", "@supabase/supabase-js": "^2.49.1",
"@tanstack/react-table": "^8.21.2", "@tanstack/react-table": "^8.21.2",
"@types/jaro-winkler": "^0.2.4",
"@xyflow/react": "12.4.2", "@xyflow/react": "12.4.2",
"ajv": "^8.17.1", "ajv": "^8.17.1",
"boring-avatars": "^1.11.2", "boring-avatars": "^1.11.2",
@@ -62,6 +63,7 @@
"embla-carousel-react": "^8.5.2", "embla-carousel-react": "^8.5.2",
"framer-motion": "^12.4.11", "framer-motion": "^12.4.11",
"geist": "^1.3.1", "geist": "^1.3.1",
"jaro-winkler": "^0.2.8",
"launchdarkly-react-client-sdk": "^3.6.1", "launchdarkly-react-client-sdk": "^3.6.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lucide-react": "^0.479.0", "lucide-react": "^0.479.0",

View File

@@ -22,6 +22,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { GraphMeta } from "@/lib/autogpt-server-api"; import { GraphMeta } from "@/lib/autogpt-server-api";
import jaro from "jaro-winkler";
interface BlocksControlProps { interface BlocksControlProps {
blocks: Block[]; blocks: Block[];
@@ -89,21 +90,73 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
}) satisfies Block, }) satisfies Block,
); );
/**
* Evaluates how well a block matches the search query and returns a relevance score.
* The scoring algorithm works as follows:
* - Returns 1 if no query (all blocks match equally)
* - Normalized query for case-insensitive matching
* - Returns 3 for exact substring matches in block name (highest priority)
* - Returns 2 when all query words appear in the block name (regardless of order)
* - Returns 1.X for blocks with names similar to query using Jaro-Winkler distance (X is similarity score)
* - Returns 0.5 when all query words appear in the block description (lowest priority)
* - Returns 0 for no match
*
* Higher scores will appear first in search results.
*/
const matchesSearch = (block: Block, query: string): number => {
if (!query) return 1;
const normalizedQuery = query.toLowerCase().trim();
const queryWords = normalizedQuery.split(/\s+/);
const blockName = block.name.toLowerCase();
const beautifiedName = beautifyString(block.name).toLowerCase();
const description = block.description.toLowerCase();
// 1. Exact match in name (highest priority)
if (
blockName.includes(normalizedQuery) ||
beautifiedName.includes(normalizedQuery)
) {
return 3;
}
// 2. All query words in name (regardless of order)
const allWordsInName = queryWords.every(
(word) => blockName.includes(word) || beautifiedName.includes(word),
);
if (allWordsInName) return 2;
// 3. Similarity with name (Jaro-Winkler)
const similarityThreshold = 0.65;
const nameSimilarity = jaro(blockName, normalizedQuery);
const beautifiedSimilarity = jaro(beautifiedName, normalizedQuery);
const maxSimilarity = Math.max(nameSimilarity, beautifiedSimilarity);
if (maxSimilarity > similarityThreshold) {
return 1 + maxSimilarity; // Score between 1 and 2
}
// 4. All query words in description (lower priority)
const allWordsInDescription = queryWords.every((word) =>
description.includes(word),
);
if (allWordsInDescription) return 0.5;
return 0;
};
return blockList return blockList
.concat(agentBlockList) .concat(agentBlockList)
.map((block) => ({
block,
score: matchesSearch(block, searchQuery),
}))
.filter( .filter(
(block: Block) => ({ block, score }) =>
(block.name.toLowerCase().includes(searchQuery.toLowerCase()) || score > 0 &&
beautifyString(block.name)
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
block.description
.toLowerCase()
.includes(searchQuery.toLowerCase())) &&
(!selectedCategory || (!selectedCategory ||
block.categories.some((cat) => cat.category === selectedCategory)), block.categories.some((cat) => cat.category === selectedCategory)),
) )
.map((block) => ({ .sort((a, b) => b.score - a.score)
.map(({ block }) => ({
...block, ...block,
notAvailable: notAvailable:
(block.uiType == BlockUIType.WEBHOOK && (block.uiType == BlockUIType.WEBHOOK &&

View File

@@ -3783,6 +3783,11 @@
dependencies: dependencies:
"@types/istanbul-lib-report" "*" "@types/istanbul-lib-report" "*"
"@types/jaro-winkler@^0.2.4":
version "0.2.4"
resolved "https://registry.yarnpkg.com/@types/jaro-winkler/-/jaro-winkler-0.2.4.tgz#fb8d9df08a984f99aef8e5a96ded3f662c738241"
integrity sha512-TNVu6vL0Z3h+hYcW78IRloINA0y0MTVJ1PFVtVpBSgk+ejmaH5aVfcVghzNXZ0fa6gXe4zapNMQtMGWOJKTLig==
"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": "@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.15" version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
@@ -7858,6 +7863,11 @@ jackspeak@^3.1.2:
optionalDependencies: optionalDependencies:
"@pkgjs/parseargs" "^0.11.0" "@pkgjs/parseargs" "^0.11.0"
jaro-winkler@^0.2.8:
version "0.2.8"
resolved "https://registry.yarnpkg.com/jaro-winkler/-/jaro-winkler-0.2.8.tgz#6727e0d0b7091e2436f9356de9bf88fad23e534a"
integrity sha512-yr+mElb6dWxA1mzFu0+26njV5DWAQRNTi5pB6fFMm79zHrfAs3d0qjhe/IpZI4AHIUJkzvu5QXQRWOw2O0GQyw==
jest-changed-files@^29.7.0: jest-changed-files@^29.7.0:
version "29.7.0" version "29.7.0"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a"