惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Hackread – Cybersecurity News, Data Breaches, AI and More
C
Check Point Blog
Hacker News: Ask HN
Hacker News: Ask HN
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
WordPress大学
WordPress大学
P
Proofpoint News Feed
V
Visual Studio Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
N
Netflix TechBlog - Medium
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 聂微东
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 叶小钗
Cisco Talos Blog
Cisco Talos Blog
S
Schneier on Security
T
Threat Research - Cisco Blogs
腾讯CDC
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Hacker News
The Hacker News
Google DeepMind News
Google DeepMind News
Microsoft Security Blog
Microsoft Security Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
GbyAI
GbyAI
N
News | PayPal Newsroom
L
LINUX DO - 最新话题
酷 壳 – CoolShell
酷 壳 – CoolShell
P
Palo Alto Networks Blog
T
Tenable Blog
S
Secure Thoughts
T
Threatpost
V2EX - 技术
V2EX - 技术
大猫的无限游戏
大猫的无限游戏
Martin Fowler
Martin Fowler
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Vercel News
Vercel News
罗磊的独立博客
P
Privacy & Cybersecurity Law Blog
Engineering at Meta
Engineering at Meta
小众软件
小众软件
Google DeepMind News
Google DeepMind News
N
News and Events Feed by Topic
Y
Y Combinator Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
Cybersecurity and Infrastructure Security Agency CISA
P
Proofpoint News Feed
L
Lohrmann on Cybersecurity
P
Privacy International News Feed
H
Heimdal Security Blog
量子位
B
Blog

积薪 - 文章

用Cloudflare Durable Objects实现延迟部署 我可能破解了日式拉面的密码 关于本站最近的更新 谣言盛行的时代,你该如何进行信息核查? 近一年来社交网络使用习惯和信息获取方式的一些改变 终于有日本驾照了 台湾观察 我目前使用的AI服务 你爱的是哪个国? 土地财政的深远影响 我们正在目睹中国文明的孤岛化 原积薪项目将于近日下线 记一次微信群吵架 终于生活在一个气候舒适的地方了 本站已进化到第三代 压垮我的最后一根稻草 不要指望银发经济 为什么是日本 日本首月记录 30岁,我做了个重大决定 什么才是规矩的博客 积薪24年1月更新 尝试留住时间的中年人 ADV换踏板,我不想要远方了 现在房价进行到哪一步了? 回老家远程一周,阳了 连肝两周,积薪8月大更新 一本典型的中式社科书 不太浪漫的田园生活
给博客加了一个搜索功能
李大毛 · 2024-08-24 · via 积薪 - 文章

新版博客上线时并没有搜索功能,但搜索一直是计划中的。本周花了两天时间,就完成了。比预想的快很多。

PGroonga

最开始打算用Postgresql插件PGroonga实现搜索。PGroonga可以对非拉丁文字的语言进行索引和检索。这个方案案例众多,文档详尽,很好实现。

我的博客有三类内容,我希望一次搜索的时候能同时检索这三个类型。

于是我创建了一个Materialized View,把文章、摄影、想法这三类内容需要检索的字段放在了同一个视图,在数据表有更新的时候trigger视图刷新。

最后给该混合视图创建index,就能实现搜索了。

前端实现起来也不难。

实际测试发现,中日文的确能搜索。但只能完整匹配。

就是说,假如有内容是“中华人民共和国”,你搜“中国”就不会有结果,必须是“中华”、“人民”这样才行。这显然不是个好用的搜索。

PGroonga可以搜索多个词,那么如果将搜索词进行拆分,就可以实现上面的需求了。

但更大的问题来了:怎么分词?

的确有很多库可以实现分词,但难道要为中日文分别采用不同的库?还得加一道判断语言的程序?太麻烦了。

虽然功能都已经写完,但我还是坚决放弃了这个路线。

Vector Search

向量搜索,就是把文本转换成向量,这个过程叫embedding。而后可以对比二者在多维空间上的距离,来判断这两段文本在语义上的相似度。

举个例子,如果你给一段文本打上标签和分数,比如“生活:0.1,技术:0.5,旅行:0.1”,而另一段文本的分数是“生活:0.1,技术:0.7,旅行:0.2”。那么这两段文本很可能在内容上是接近的,都是技术类内容。

实际的向量数多达上千,这只是一个简化的理解。

向量搜索的优势是不再局限于特定关键词的匹配,而是从语义上进行检索。比如一个菜谱数据库,你在搜索“大盘鸡”的时候,可能也想看看其他新疆的菜谱,或者其他以鸡为主料的菜。这些向量搜索都可以实现。

虽然embedding的过程需要借助AI(本站使用OpenAI的text-embedding-ada-002模型),有成本。但同样的向量数据也可以用来做内容推荐,这也是我未来要加的功能。

借助Supabase Edge Function,可以很容易实现自动生成向量数据并存储的功能。

虽然向量搜索可以搜索内容的含义,但有些时候准确度不如关键词搜索。拿上面的例子来说,也许我就是想要“大盘鸡”的信息。但向量搜索会给出一堆不是大盘鸡的菜谱。

最好的方案,还是以关键词为主,语义搜索为辅。这时候meilisearch再次进入我的视线。

meilisearch

其实一开始就考虑过meilisearch,但当时秉持能简单就简单的思想,暂时搁置了。经过一番摸索,发现meilisearch还是当前最优方案:

  1. 自带多语言分词和索引,你只管添加数据,不用管具体实现;

  2. 可以使用OpenAI的API生成向量数据,实现语义搜索;

  3. 可以搜索相似内容,用于内容推荐。

并且设置了OpenAI的key后,并不需要你手动处理embedding的过程,都是自动的。

当前方案

写了一个Edge Function,在对应的表发生了INSERT、UPDATE、DELETE时触发。前两个操作会把新增的数据发送给meilisearch服务器,后一个则是删除对应的数据。

import { serve } from "https://deno.land/std@0.224.0/http/server.ts";

const MEILI_URL = Deno.env.get('MEILI_URL');
const MEILI_KEY = Deno.env.get('MEILI_KEY');

interface Record {
  id: string;
  lang?: string;
  slug?: string;
  title?: string;
  subtitle?: string;
  abstract?: string;
  content_text?: string;
  topic?: string;
  is_draft?: boolean;
}

interface Payload {
  type: 'INSERT' | 'UPDATE' | 'DELETE';
  table: string;
  schema: string;
  record?: Record;
  old_record?: Record;
}

async function handleMeilisearch(payload: Payload) {
  const { type, table, record, old_record } = payload;

  let url = `${MEILI_URL}/indexes/${table}/documents`;
  let method = 'POST';
  let body;

  if (type === 'DELETE' || (type === 'UPDATE' && !old_record.is_draft && record.is_draft)) {
    url = `${url}/${old_record.id}`;
    method = 'DELETE';
  } else if (type === 'INSERT' || type === 'UPDATE') {
    if (record.is_draft) {
      console.log(`跳过索引操作:${table} 是草稿状态`);
      return { skipped: true, reason: 'Draft' };
    }

    const fields = {
      article: ['id', 'lang', 'slug', 'title', 'subtitle', 'abstract', 'content_text', 'topic'],
      photo: ['id', 'slug', 'lang', 'title', 'abstract', 'content_text', 'topic'],
      thought: ['id', 'slug', 'content_text', 'topic']
    };

    body = JSON.stringify([
      fields[table as keyof typeof fields].reduce((obj, field) => {
        if (record[field as keyof Record] !== undefined) {
          obj[field] = record[field as keyof Record];
        }
        return obj;
      }, {} as Record)
    ]);

    method = type === 'UPDATE' ? 'PUT' : 'POST';
  }

  const response = await fetch(url, {
    method,
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${MEILI_KEY}`
    },
    body
  });

  if (!response.ok) {
    throw new Error(`Meilisearch操作失败: ${response.statusText}`);
  }

  return response.json();
}

serve(async (req) => {
  try {
    const payload: Payload = await req.json();
    const result = await handleMeilisearch(payload);
    return new Response(JSON.stringify(result), {
      headers: { 'Content-Type': 'application/json' }
    });
  } catch (error) {
    console.error('处理请求时发生错误:', error);
    return new Response(JSON.stringify({ error: error.message }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' }
    });
  }
});

等到数据都传输到meilisearch存储为document,等待索引完成,就可以进行搜索了。

你可以自行设置如何搜索,设置高亮的字段,设定向量搜索所占的比重等等。将下面的代码作为body发送给meilisearch的/multi-search endpoint,具体该怎么搜索可以看文档来决定:

queries: [
  {
    indexUid: "article",
    q: query,
    limit: 10,
    attributesToCrop: ["abstract", "content_text"],
    cropLength: 24,
    cropMarker: "...",
    attributesToHighlight: ["title", "abstract", "content_text", "topic"],
    highlightPreTag: "<span class=\"text-violet-600\">",
    highlightPostTag: "</span>",
    showRankingScore: true,
    hybrid: {
      embedder: "default",
      semanticRatio: 0.4
    }
  },
  {
    indexUid: "photo",
    q: query,
    limit: 15,
    attributesToCrop: ["abstract", "content_text"],
    cropLength: 24,
    cropMarker: "...",
    attributesToHighlight: ["title", "abstract", "content_text", "topic"],
    highlightPreTag: "<span class=\"text-violet-600\">",
    highlightPostTag: "</span>",
    showRankingScore: true,
    hybrid: {
      embedder: "default",
      semanticRatio: 0.5
    }
  },
  {
    indexUid: "thought",
    q: query,
    limit: 5,
    attributesToCrop: ["content_text"],
    cropLength: 24,
    cropMarker: "...",
    attributesToHighlight: ["content_text", "topic"],
    highlightPreTag: "<span class=\"text-violet-600\">",
    highlightPostTag: "</span>",
    showRankingScore: true,
    hybrid: {
      embedder: "default",
      semanticRatio: 0.5
    }
  }
]

最后你可以在前端过滤一下结果,按照rankingScore进行排序。这样一个支持关键词和模糊搜索、能检索多个类型内容的简易搜索引擎就好了。

具体效果可以点击导航栏搜索图标体验。