2409 字
12 分钟
AI Search接入博客

前言#

偶然间看到Cloudflare的ai search,不需要自己配置向量化模型和问答模型就可以完成ai知识库配置,只需导入内容即可,遂尝试构建自己的ai知识库,并接入博客作为ai增强搜索以及ai知识库问答助手

知识库AI助手接入#

搭建#

在cloudflare中AI -->AI搜索中创建项目,数据来源可选R2存储桶网站,先搭建知识库,所以这里选R2存储桶作为数据来源

知识库构建-1

我习惯使用Obsidian作为笔记工具,所以使用ObsidianRemotely-Save插件将本地笔记同步到R2存储桶,然后下一步给AI网关和API等授权,cloudflare会自动处理创建网关等操作,并在ai搜索项目首次创建时进行一次索引构建,后续会定期同步数据源更新索引,也可以手动同步。

接入#

在项目设置中启用Public URL,完成自定义样式设置,在src/layouts/Layout.astro文件约159行位置插入如下代码,替换<hash>和自定义的样式设置

src/layouts/Layout.astro
<!-- 在布局文件末尾,</body> 之前添加 -->
<script type="module" src="https://<hash>.search.ai.cloudflare.com/assets/v0.0.25/search-snippet.es.js"></script>
<chat-bubble-snippet
api-url="https://<hash>.search.ai.cloudflare.com/"
placeholder="Search..."
theme="light"
hide-branding="true">
</chat-bubble-snippet>
<style is:global>
/* 可选:自定义样式,作用全局 */
chat-bubble-snippet {
--search-snippet-primary-color: #74a7d8;
--search-snippet-primary-hover: #92c6f7;
--search-snippet-focus-ring: #74a7d8;
--search-snippet-surface: #ffffff;
--search-snippet-hover-background: #ffffff;
--search-snippet-border-color: #f0f9ff;
}
</style>

效果如下:

知识库构建-2

AI搜索接入#

搭建#

流程同上,这里作为网页内容搜索,所以数据来源选择网站,设置完sitemap后启用Public URL

接入#

编辑src/components/Search.svelte,这里给出我用ai生成的代码,仅供参考,替换<hash>和外观配置,可能存在一些小bug,请自行修改优化:

src/components/Search.svelte
<script lang="ts">
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import Icon from "@iconify/svelte";
import { url } from "@utils/url-utils.ts";
import { onMount, tick } from "svelte";
import type { SearchResult } from "@/global";
let keywordDesktop = "";
let keywordMobile = "";
let result: SearchResult[] = [];
let isSearching = false;
let pagefindLoaded = false;
368 collapsed lines
let initialized = false;
let showAISearch = false; // 新增:控制AI搜索显示状态
let aiSearchContainer: HTMLElement; // 新增:AI搜索容器引用
const fakeResult: SearchResult[] = [
{
url: url("/"),
meta: {
title: "This Is a Fake Search Result",
},
excerpt:
"Because the search cannot work in the <mark>dev</mark> environment.",
},
{
url: url("/"),
meta: {
title: "If You Want to Test the Search",
},
excerpt: "Try running <mark>npm build && npm preview</mark> instead.",
},
];
// 新增:点击外部关闭AI搜索框
const handleClickOutside = (event: MouseEvent) => {
if (!showAISearch || !aiSearchContainer) return;
const target = event.target as HTMLElement;
const searchBar = document.getElementById("search-bar");
const aiButton = searchBar?.querySelector('button[aria-label="AI Search"]');
// 检查点击是否在AI搜索容器、搜索框或AI按钮内部
const isClickInside =
aiSearchContainer.contains(target) ||
searchBar?.contains(target) ||
aiButton?.contains(target);
if (!isClickInside) {
showAISearch = false;
}
};
// 新增:切换AI搜索显示
const toggleAISearch = async (event?: MouseEvent) => {
// 阻止事件冒泡,避免立即触发外部点击检测
event?.stopPropagation();
showAISearch = !showAISearch;
if (showAISearch) {
// 等待DOM更新,确保容器已渲染
await tick();
// 添加全局点击监听器
setTimeout(() => {
document.addEventListener('click', handleClickOutside);
}, 0);
// 如果开启了AI搜索,加载Cloudflare AI搜索库
if (!document.querySelector('script[src*="search.ai.cloudflare.com"]')) {
const script = document.createElement('script');
script.type = 'module';
script.src = 'https://<hash>.search.ai.cloudflare.com/assets/v0.0.25/search-snippet.es.js';
document.head.appendChild(script);
// 添加样式
const style = document.createElement('style');
style.textContent = `
search-bar-snippet {
--search-snippet-primary-color: #74a7d8;
--search-snippet-primary-hover: #92c6f7;
--search-snippet-focus-ring: #74a8d8;
}
`;
document.head.appendChild(style);
}
} else {
// 移除全局点击监听器
document.removeEventListener('click', handleClickOutside);
}
};
// 新增:组件卸载时清理事件监听器
const cleanup = () => {
document.removeEventListener('click', handleClickOutside);
};
const togglePanel = () => {
const panel = document.getElementById("search-panel");
panel?.classList.toggle("float-panel-closed");
};
const setPanelVisibility = (show: boolean, isDesktop: boolean): void => {
const panel = document.getElementById("search-panel");
if (!panel || !isDesktop) return;
if (show) {
panel.classList.remove("float-panel-closed");
} else {
panel.classList.add("float-panel-closed");
}
};
const search = async (keyword: string, isDesktop: boolean): Promise<void> => {
if (!keyword) {
setPanelVisibility(false, isDesktop);
result = [];
return;
}
if (!initialized) {
return;
}
isSearching = true;
try {
let searchResults: SearchResult[] = [];
if (import.meta.env.PROD && pagefindLoaded && window.pagefind) {
const response = await window.pagefind.search(keyword);
searchResults = await Promise.all(
response.results.map((item) => item.data()),
);
} else if (import.meta.env.DEV) {
searchResults = fakeResult;
} else {
searchResults = [];
console.error("Pagefind is not available in production environment.");
}
result = searchResults;
setPanelVisibility(result.length > 0, isDesktop);
} catch (error) {
console.error("Search error:", error);
result = [];
setPanelVisibility(false, isDesktop);
} finally {
isSearching = false;
}
};
onMount(() => {
const initializeSearch = () => {
initialized = true;
pagefindLoaded =
typeof window !== "undefined" &&
!!window.pagefind &&
typeof window.pagefind.search === "function";
console.log("Pagefind status on init:", pagefindLoaded);
if (keywordDesktop) search(keywordDesktop, true);
if (keywordMobile) search(keywordMobile, false);
};
if (import.meta.env.DEV) {
console.log(
"Pagefind is not available in development mode. Using mock data.",
);
initializeSearch();
} else {
document.addEventListener("pagefindready", () => {
console.log("Pagefind ready event received.");
initializeSearch();
});
document.addEventListener("pagefindloaderror", () => {
console.warn(
"Pagefind load error event received. Search functionality will be limited.",
);
initializeSearch(); // Initialize with pagefindLoaded as false
});
// Fallback in case events are not caught or pagefind is already loaded by the time this script runs
setTimeout(() => {
if (!initialized) {
console.log("Fallback: Initializing search after timeout.");
initializeSearch();
}
}, 2000); // Adjust timeout as needed
}
// 返回清理函数
return cleanup;
});
$: if (initialized && keywordDesktop) {
(async () => {
await search(keywordDesktop, true);
})();
}
$: if (initialized && keywordMobile) {
(async () => {
await search(keywordMobile, false);
})();
}
// 新增:当AI搜索关闭时,移除事件监听器
$: if (!showAISearch) {
document.removeEventListener('click', handleClickOutside);
}
</script>
<!-- 桌面版搜索容器 -->
<div class="hidden lg:flex flex-col relative">
<!-- search bar for desktop view -->
<div id="search-bar" class="flex transition-all items-center h-11 mr-2 rounded-lg
bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]
dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10
">
<Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>
<input placeholder="{i18n(I18nKey.search)}" bind:value={keywordDesktop} on:focus={() => search(keywordDesktop, true)}
class="transition-all pl-10 text-sm bg-transparent outline-0
h-full w-40 active:w-60 focus:w-60 text-black/50 dark:text-white/50"
>
<!-- 新增:AI搜索按钮 -->
<button
on:click={toggleAISearch}
aria-label="AI Search"
class="ml-2 p-2 rounded-lg transition-colors duration-200 hover:bg-black/[0.08] dark:hover:bg-white/10"
style:background-color={showAISearch ? '#92c6f7' : 'transparent'}
>
<Icon
icon="material-symbols:smart-toy-outline"
class="text-[1.25rem] transition-colors {showAISearch ? 'text-white' : 'text-black/50 dark:text-white/50'}"
></Icon>
</button>
</div>
<!-- AI搜索栏(桌面版,在搜索框下方弹出) -->
{#if showAISearch}
<div
bind:this={aiSearchContainer}
class="absolute top-full left-0 right-0 mt-2 z-50"
role="dialog"
tabindex="0"
aria-modal="true"
on:click|stopPropagation
on:keydown={(e) => {
if (e.key === "Escape") {
showAISearch = false;
}
}}
>
<search-bar-snippet placeholder="使用AI搜索..." api-url="https://<hash>.search.ai.cloudflare.com/" hide-branding="true"></search-bar-snippet>
</div>
{/if}
</div>
<!-- toggle btn for phone/tablet view -->
<button on:click={togglePanel} aria-label="Search Panel" id="search-switch"
class="btn-plain scale-animation lg:!hidden rounded-lg w-11 h-11 active:scale-90">
<Icon icon="material-symbols:search" class="text-[1.25rem]"></Icon>
</button>
<!-- search panel -->
<div id="search-panel" class="float-panel float-panel-closed search-panel absolute md:w-[30rem]
top-20 left-4 md:left-[unset] right-4 shadow-2xl rounded-2xl p-2">
<!-- search bar inside panel for phone/tablet -->
<div id="search-bar-inside" class="flex relative lg:hidden transition-all items-center h-11 rounded-xl
bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]
dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10
">
<Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>
<input placeholder="Search" bind:value={keywordMobile}
class="pl-10 absolute inset-0 text-sm bg-transparent outline-0
focus:w-60 text-black/50 dark:text-white/50"
>
<!-- 新增:移动端AI搜索按钮 -->
<button
on:click={toggleAISearch}
aria-label="AI Search"
class="absolute right-2 p-2 rounded-lg transition-colors duration-200 hover:bg-black/[0.08] dark:hover:bg-white/10"
style:background-color={showAISearch ? '#92c6f7' : 'transparent'}
>
<Icon
icon="material-symbols:smart-toy-outline"
class="text-[1.25rem] transition-colors {showAISearch ? 'text-white' : 'text-black/50 dark:text-white/50'}"
></Icon>
</button>
</div>
<!-- AI搜索栏(移动端,在搜索框下方) -->
{#if showAISearch}
<div
class="lg:hidden mt-2 mb-4 relative z-10"
role="dialog"
aria-label="AI Search"
tabindex="-1"
on:click|stopPropagation
on:keydown={(e) => {
if (e.key === "Escape") {
showAISearch = false;
}
}}
>
<!-- 包裹容器,确保AI搜索组件能够正常展开 -->
<div class="ai-search-container-mobile">
<search-bar-snippet placeholder="使用AI搜索..." api-url="https://<hash>.search.ai.cloudflare.com/" hide-branding="true"></search-bar-snippet>
</div>
</div>
{/if}
<!-- search results -->
{#each result as item}
<a href={item.url}
class="transition first-of-type:mt-2 lg:first-of-type:mt-0 group block
rounded-xl text-lg px-3 py-2 hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)]">
<div class="transition text-90 inline-flex font-bold group-hover:text-[var(--primary)]">
{item.meta.title}<Icon icon="fa6-solid:chevron-right" class="transition text-[0.75rem] translate-x-1 my-auto text-[var(--primary)]"></Icon>
</div>
<div class="transition text-sm text-50">
{@html item.excerpt}
</div>
</a>
{/each}
</div>
<style>
input:focus {
outline: 0;
}
.search-panel {
max-height: calc(100vh - 100px);
overflow-y: auto;
}
/* Cloudflare AI搜索组件样式 */
search-bar-snippet {
--search-snippet-primary-color: #74a7d8;
--search-snippet-primary-hover: #92c6f7;
--search-snippet-focus-ring: #74a8d8;
width: 100%;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
/* 移动端AI搜索容器样式 */
.ai-search-container-mobile {
position: relative;
width: 100%;
min-height: 60px; /* 确保有最小高度 */
}
/* 确保AI搜索组件在移动端能够正常展开 */
.ai-search-container-mobile search-bar-snippet {
position: static !important; /* 覆盖可能的绝对定位 */
max-height: none !important; /* 移除高度限制 */
overflow: visible !important; /* 确保内容可见 */
}
/* 针对Cloudflare AI搜索组件的特定样式 */
.ai-search-container-mobile ::ng-deep search-bar-snippet,
.ai-search-container-mobile ::slotted(search-bar-snippet) {
position: static !important;
max-height: none !important;
overflow: visible !important;
}
/* 确保搜索结果列表能够正常显示 */
.search-panel {
overflow: visible; /* 允许内容溢出 */
}
/* 当AI搜索激活时,调整搜索面板样式 */
.search-panel:has(.ai-search-container-mobile search-bar-snippet[expanded]) {
overflow-y: auto; /* 恢复滚动 */
max-height: 80vh; /* 增加最大高度 */
}
</style>

效果如下: AI搜索构建-1

完结撒花#

cf的AI搜索省去了自己配置嵌入模型和前端查询响应模型的繁琐步骤,并且每天提供10k神经元额度,这个评价必须给到夯。目前模型响应速度和国内访问速度都还不错,写完笔记点点Remotely-Save就能推送到知识库😋

题外话#

AI发展真的太快了,从最初chatgpt发布,只觉得这玩意儿能聊天还挺好玩,受限于没有境外信用卡和网络环境,对gpt的使用仅限于网页聊天。到deepseek发布,第一次知道ai可以通过api接入到自己的程序里面,那时候deepseek每个月还有10元的免费额度,低廉的调用成本带来了巨量的使用量,可能还有竞争对手的攻击,让其api接口一度瘫痪,甚至连官网的reasoner模型都基本无法响应。后来国内各厂商也是紧追其后,阿里、字节等都发布了自己的模型。

从高中到大学短短几年,见证了ai从娱乐聊天机器人,变成了实打实的生产力工具,自己对ai的依赖也是越来越重。初学逆向时,我经常把整段的代码贴给ai,可往往一个花指令就能骗过ai,到现在比赛,配好agent,配好ida-mcp和wsl,接个powerful的模型api,ai接管了从代码分析到动态调试、接收报错继续修正的所有流程,除了烧token外几乎不需要额外介入。很难想象,赛场上隐藏在屏幕之下和你对线的,可能不是碳基生物。

openclaw发布后,总能刷到“付费上门安装龙虾”或是“付费卸载龙虾”一类的推文,且不论是博人眼球亦或是确有此事,ai对日常生活渗透之快是不可置否的,使用技术的门槛降低也许是好事,但对于我而言,对ai的依赖总是无声无息的消磨着我学习的欲望,盯着电脑花几个小时调试程序,不如问ai来的快,甚至容易产生”这就是我的水平”的错觉,希望自己能时刻保持清醒,继续学习🫡

AI Search接入博客
https://blog.colix.cn/posts/博客/cloudflare-ai-search/main/
作者
Q
发布于
2026-03-18
许可协议
CC BY-NC-SA 4.0