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

推荐订阅源

T
The Blog of Author Tim Ferriss
S
Schneier on Security
H
Help Net Security
aimingoo的专栏
aimingoo的专栏
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MongoDB | Blog
MongoDB | Blog
云风的 BLOG
云风的 BLOG
H
Hacker News: Front Page
C
Check Point Blog
P
Privacy International News Feed
IT之家
IT之家
爱范儿
爱范儿
AWS News Blog
AWS News Blog
The Hacker News
The Hacker News
Stack Overflow Blog
Stack Overflow Blog
Project Zero
Project Zero
Microsoft Azure Blog
Microsoft Azure Blog
量子位
The Cloudflare Blog
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Cybersecurity and Infrastructure Security Agency CISA
I
Intezer
Martin Fowler
Martin Fowler
Scott Helme
Scott Helme
酷 壳 – CoolShell
酷 壳 – CoolShell
A
Arctic Wolf
T
Threat Research - Cisco Blogs
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
The Exploit Database - CXSecurity.com
B
Blog
Simon Willison's Weblog
Simon Willison's Weblog
U
Unit 42
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Microsoft Security Blog
Microsoft Security Blog
博客园_首页
J
Java Code Geeks
K
Kaspersky official blog
Webroot Blog
Webroot Blog
C
CERT Recently Published Vulnerability Notes
H
Heimdal Security Blog
G
Google Developers Blog
T
Tor Project blog
W
WeLiveSecurity
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Blog — PlanetScale
Blog — PlanetScale
小众软件
小众软件
有赞技术团队
有赞技术团队

lucifer的网络博客

加密交易的风险管理:仓位、止损与资金分配 | lucifer的网络博客 永续合约:无到期日的杠杆交易是如何运作的 | lucifer的网络博客 代币经济学:如何判断一个代币值不值得买 | lucifer的网络博客 稳定币机制全解:从 USDT 到 UST 崩盘的启示 DeFi 保险:谁来保护你的链上资产? | lucifer的网络博客 Claude Code 高级指南:驾驭上下文的艺术 | lucifer的网络博客 NotebookLM:AI 驱动的知识炼金术,瞬间转化复杂信息为易懂洞见 | lucifer的网络博客 当下流动性池分析 | lucifer的网络博客 苹果AI转型:Siri升级与生态潜力 | lucifer的网络博客 我们是如何让 JSON.stringify 的速度提升两倍以上的 | lucifer的网络博客 ACP:AI 代理协作协议在 Web3 空投自动化中的应用 | lucifer的网络博客 限制即提示:算法解题的隐秘指引 | lucifer的网络博客 如何阅读财报:从入门到进阶指南 | lucifer的网络博客 Freqtrade 量化风暴:Python 铸就的加密交易自动化帝国 | lucifer的网络博客 DePIN 觉醒:Web3 如何让你的闲置硬件变黄金 | lucifer的网络博客 自己动手制作 Web3 代币估值表 | lucifer的网络博客 当 RWA 遇上幸福小区玉米 | lucifer的网络博客 探索 DeFi 借贷与收益率交易的无限潜力 | lucifer的网络博客 Uniswap V4:更灵活、更高效的去中心化交易所 | lucifer的网络博客 Web3 科学家之如何查看鲸鱼挂单数据 | lucifer的网络博客 抖音也开始搞外卖大战了? | lucifer的网络博客 Web3 的 Anti MEV:深入剖析“夹”与反制之道 | lucifer的网络博客 《小菜的交易系统》-一个完整的简易的交易系统 | lucifer的网络博客 回炉重铸, 91 天见证不一样的自己(第 16 期)
从零构建一个真正工作的简单搜索引擎 | lucifer的网络博客
2025-11-29 · via lucifer的网络博客

大家好,我是 Lucifer。今天我们来聊聊如何用 Python 构建一个简单的搜索引擎。为什么写这篇?因为教程要么是黑箱框架(Elasticsearch 配置一堆),要么是浅尝辄止(就字符串匹配)。我希望带你从零起步,实现一个真正能工作的版本:它能用倒排索引存储文档、多种方式切分文本(tokenization)、计算权重,并根据相关度排序返回结果。

这篇文章聚焦核心:倒排索引 + tokenization + 权重系统。我们用 Python 的 dict 模拟一切,不需外部库,只用内置字符串处理和 math。边看边敲代码,预计阅读+实践:20 分钟。Python 3 环境准备好?走起!

第一步:理解核心概念

什么是倒排索引?

搜索引擎的核心是倒排索引(Inverted Index)。正常索引像“文档 ID → 内容”(书架上按书号排);倒排索引反过来:“关键词 → 文档列表”(按主题直奔书堆)。

为什么重要?直接扫描所有文档太慢(O(总词数)),倒排索引让搜索 O(查询词数),超快。举例:两篇文档

  • Doc 1: 标题 “Python 入门”,正文 “Python 是伟大的编程语言”
  • Doc 2: 标题 “Java vs Python”,正文 “Java 也很棒,但 Python 更简单”

简单倒排索引(不带权重):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"python": [1, 2], # 包含 python 的文档 ID
"入门": [1],
"是": [1],
"伟大的": [1],
"编程": [1],
"语言": [1],
"java": [2],
"vs": [2],
"很": [2],
"棒": [2],
"但": [2],
"更": [2],
"简单": [2]
}

查询 “Python 简单”:查 “python” → [1,2],”简单” → [2],取交集 [2],返回 Doc 2。就是这么直观!

但光存 ID 不够——文档长短不一,词位置不同,得加权重来精炼相关度。自然过渡:倒排索引从“存列表”升级到“存带权重的列表”,让标题匹配 > 正文匹配,长词 > 短词。接下来我们细聊怎么切词(tokenization),这是构建索引的第一步。

Tokenization:怎么切分文本?

文本不是整体处理的,得切成小“token”(词或子串),供索引用。我们用三种 tokenizer,从精确到模糊,覆盖不同查询场景。每个都简单实现,下面直接上代码+解释。

先统一“预处理”:转小写、去标点、过滤短词(<2 字符)。代码:

1
2
3
def _normalize(self, text: str) -> str:
"""统一处理:小写 + 去非字母/数字/空格"""
return ''.join(c.lower() for c in text if c.isalnum() or c == ' ').strip()
  1. WordTokenizer:按空格切完整词,精确匹配。权重高(20),因为最可靠。

    1
    2
    3
    def _word_tokenize(self, text: str) -> List[str]:
    norm = self._normalize(text)
    return [w for w in norm.split() if len(w) >= 2]

    例:”Python 入门” → [‘python’, ‘入门’]

  2. PrefixTokenizer:生成词的前缀,帮模糊搜索(如 “pyth” 匹配 “python”)。权重中(5)。

    1
    2
    3
    4
    5
    6
    7
    def _prefix_tokenize(self, text: str, min_len=4) -> List[str]:
    words = self._word_tokenize(text)
    tokens = []
    for word in words:
    for i in range(min_len, len(word) + 1):
    tokens.append(word[:i])
    return tokens

    例:”Python” → [‘pyth’, ‘pytho’, ‘python’](min_len=4)

  3. NGramsTokenizer:切成固定长字符块(3-gram),最模糊,抓拼写变体。权重低(1)。

    1
    2
    3
    def _ngram_tokenize(self, text: str, n=3) -> List[str]:
    norm = self._normalize(text).replace(' ', '')
    return [norm[i:i+n] for i in range(len(norm) - n + 1) if len(norm[i:i+n]) == n]

    例:”Python” → [‘pyt’, ‘yth’, ‘tho’, ‘hon’]

这些 tokenizer 在索引和查询时都跑,确保一致。为什么三种?精确查询用 Word,部分输入用 Prefix,拼错用 NGrams。简单吧?现在,权重系统就建立在这些 token 上。

权重系统:怎么让匹配“聪明”起来?

权重让倒排索引从“有/无”变“多相关”。核心公式(每 token):

  • 基础权重 = 字段权重(标题=10,正文=1) × tokenizer 权重(Word=20,Prefix=5,NGrams=1) × ceil(sqrt(词长))(长词更具体,如 “programming” len=11 → ceil(sqrt(11))≈4)

例:”python” (len=6, sqrt≈2.45→3) 在 Doc1 标题 Word:10 × 20 × 3 = 600;在 Doc2 正文 Prefix:1 × 5 × 3 = 15。

搜索时,文档总分 = sum(所有匹配 token 的权重) × 调整因子:

  • × (1 + log(独特匹配 token 数)):多样性——匹配多词更好。
  • × (1 + log(平均匹配权重)):质量——高权重匹配更好。
  • / (1 + log(文档总词数)):长度惩罚——长文别刷分。

归一化:总分 / max(可能分),范围 [0,1]。这样,”Python in title” (高场权重) 远超 “python in body” (低场权重),Prefix 匹配也加分但不主导。

一句话总结:权重综合位置(标题>正文)精确度(Word>模糊)具体性(长词>短)多样/质量/长度,让排序更准、更像 Google。

第二步:准备数据结构

用类 SimpleSearchEngine 封装。核心两个 dict:

  • documents: {doc_id: {“title”: str, “body”: str}} —— 存原内容。
  • index: {token: {tokenizer_weight: [(doc_id, field, final_weight)]}} —— 倒排,token 指向分组的带权列表。

例(加 Doc1 后,简化版 index):

1
2
3
4
5
6
7
8
9
10
11
{
"python": {
20: [(1, 'title', 600)], # Word, 标题, 10*20*3
5: [(1, 'title', 150)], # Prefix 全词
# ... NGrams 类似
},
"入门": {
20: [(1, 'title', 300)] # len=2, sqrt≈1.4→2, 10*20*2
},
# 正文 token 类似,但 field_weight=1
}

tokenizer 存为 dict:{‘word’: (func, 20), …}。代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from collections import defaultdict
import math
from typing import Dict, List, Tuple, Any

class SimpleSearchEngine:
def __init__(self):
self.documents: Dict[int, Dict[str, Any]] = {}
self.index: Dict[str, Dict[int, List[Tuple[int, str, int]]]] = {}
self.tokenizers = {
'word': (self._word_tokenize, 20),
'prefix': (self._prefix_tokenize, 5),
'ngram': (self._ngram_tokenize, 1)
}


透明吧?每个 token 的内层 dict 按 tokenizer_weight 分组,便于搜索时过滤低权。

第三步:添加文档

add_document:存文档,按字段跑所有 tokenizer,算权重,塞进 index。用 set(tokens) 去重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def add_document(self, doc_id: int, title: str, body: str, doc_type: str = 'default'):
if doc_id in self.documents:
raise ValueError("文档 ID 已存在")
self.documents[doc_id] = {'title': title, 'body': body, 'type': doc_type}


for field, content, field_weight in [('title', title, 10), ('body', body, 1)]:
for t_name, (tokenize_fn, t_weight) in self.tokenizers.items():
tokens = tokenize_fn(content)
for token in set(tokens):
final_weight = field_weight * t_weight * math.ceil(math.sqrt(len(token)))
if token not in self.index:
self.index[token] = defaultdict(list)
self.index[token][t_weight].append((doc_id, field, final_weight))

例:跑 Doc1,”python” 在标题 Word 存 (1, ‘title’, 600)。O(总 token 数),快!

第四步:执行搜索

search:切查询 token(所有 tokenizer),找候选文档(有匹配),算总分,排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def search(self, query: str, top_k: int = 5, min_token_weight: int = 1) -> List[Tuple[int, float, str]]:

query_tokens = set()
for _, (tokenize_fn, _) in self.tokenizers.items():
query_tokens.update(tokenize_fn(query))
if not query_tokens:
return []


candidate_docs = set()
for token in query_tokens:
if token in self.index:
for w, entries in self.index[token].items():
if w >= min_token_weight:
for doc_id, _, _ in entries:
candidate_docs.add(doc_id)
if not candidate_docs:
return []


results = []
for doc_id in candidate_docs:
doc = self.documents[doc_id]
total_tokens = len(self._word_tokenize(doc['title'] + ' ' + doc['body']))

matched_weights = []
unique_tokens = set()
for token in query_tokens:
if token in self.index:
for w, entries in self.index[token].items():
for d_id, _, weight in entries:
if d_id == doc_id:
matched_weights.append(weight)
unique_tokens.add(token)
break

if not matched_weights:
continue

sum_weight = sum(matched_weights)
diversity = 1 + math.log(1 + len(unique_tokens))
avg_weight = sum_weight / len(matched_weights)
quality = 1 + math.log(1 + avg_weight)
length_penalty = 1 + math.log(1 + total_tokens)
score = (sum_weight * diversity * quality / length_penalty) / max(1, sum_weight)

title_preview = doc['title'][:20] + "..."
results.append((doc_id, score, title_preview))

results.sort(key=lambda x: x[1], reverse=True)
return results[:top_k]

例:查询 “Python 简单”(假设 Doc1/2 已加)。

  • Doc1:匹配 “python” (600 title Word + 低 Prefix),独特2,sum≈650,总词≈5,score≈0.85(标题高)。
  • Doc2:匹配 “python” (15 body Prefix) + “简单” (低),独特2,sum≈30,总词≈7,score≈0.25。
    排序:Doc1 > Doc2。

清晰了?sum_weight 抓“量”,diversity/quality 抓“质”,penalty 抓“简”。

第五步:完整代码与扩展建议

完整版(聚焦核心,≈120 行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
from collections import defaultdict
import math
from typing import Dict, List, Tuple, Any

class SimpleSearchEngine:
def __init__(self):
self.documents: Dict[int, Dict[str, Any]] = {}
self.index: Dict[str, Dict[int, List[Tuple[int, str, int]]]] = {}
self.tokenizers = {
'word': (self._word_tokenize, 20),
'prefix': (self._prefix_tokenize, 5),
'ngram': (self._ngram_tokenize, 1)
}

def _normalize(self, text: str) -> str:
return ''.join(c.lower() for c in text if c.isalnum() or c == ' ').strip()

def _word_tokenize(self, text: str) -> List[str]:
norm = self._normalize(text)
return [w for w in norm.split() if len(w) >= 2]

def _prefix_tokenize(self, text: str, min_len=4) -> List[str]:
words = self._word_tokenize(text)
tokens = []
for word in words:
for i in range(min_len, len(word) + 1):
tokens.append(word[:i])
return tokens

def _ngram_tokenize(self, text: str, n=3) -> List[str]:
norm = self._normalize(text).replace(' ', '')
return [norm[i:i+n] for i in range(len(norm) - n + 1) if len(norm[i:i+n]) == n]

def add_document(self, doc_id: int, title: str, body: str, doc_type: str = 'default'):
if doc_id in self.documents:
raise ValueError("文档 ID 已存在")
self.documents[doc_id] = {'title': title, 'body': body, 'type': doc_type}
for field, content, field_weight in [('title', title, 10), ('body', body, 1)]:
for t_name, (tokenize_fn, t_weight) in self.tokenizers.items():
tokens = tokenize_fn(content)
for token in set(tokens):
final_weight = field_weight * t_weight * math.ceil(math.sqrt(len(token)))
if token not in self.index:
self.index[token] = defaultdict(list)
self.index[token][t_weight].append((doc_id, field, final_weight))

def search(self, query: str, top_k: int = 5, min_token_weight: int = 1) -> List[Tuple[int, float, str]]:
query_tokens = set()
for _, (tokenize_fn, _) in self.tokenizers.items():
query_tokens.update(tokenize_fn(query))
if not query_tokens:
return []
candidate_docs = set()
for token in query_tokens:
if token in self.index:
for w, entries in self.index[token].items():
if w >= min_token_weight:
for doc_id, _, _ in entries:
candidate_docs.add(doc_id)
if not candidate_docs:
return []
results = []
for doc_id in candidate_docs:
doc = self.documents[doc_id]
total_tokens = len(self._word_tokenize(doc['title'] + ' ' + doc['body']))
matched_weights = []
unique_tokens = set()
for token in query_tokens:
if token in self.index:
for w, entries in self.index[token].items():
for d_id, _, weight in entries:
if d_id == doc_id:
matched_weights.append(weight)
unique_tokens.add(token)
break
if not matched_weights:
continue
sum_weight = sum(matched_weights)
diversity = 1 + math.log(1 + len(unique_tokens))
avg_weight = sum_weight / len(matched_weights)
quality = 1 + math.log(1 + avg_weight)
length_penalty = 1 + math.log(1 + total_tokens)
score = (sum_weight * diversity * quality / length_penalty) / max(1, sum_weight)
title_preview = doc['title'][:20] + "..."
results.append((doc_id, score, title_preview))
results.sort(key=lambda x: x[1], reverse=True)
return results[:top_k]


if __name__ == "__main__":
engine = SimpleSearchEngine()
engine.add_document(1, "Python 入门", "Python 是伟大的编程语言", 'article')
engine.add_document(2, "Java vs Python", "Java 也很棒,但 Python 更简单", 'article')
results = engine.search("Python 简单")
for doc_id, score, title in results:
print(f"Doc {doc_id}: {title} (分数: {score:.2f})")

打印:

1
2
Doc 1: Python 入门... (分数: 6.98)
Doc 2: Java vs Python... (分数: 5.91)

Doc1 分高(标题匹配)。

扩展? 加字段权重、存 pickle、加真 TF-IDF (IDF=log(总doc/含词doc))。

总结

恭喜!你建了个带权重的搜索引擎,核心逻辑透明,代码只有 100 多行。倒排索引存 token→文档+权,tokenization 切精确/模糊,权重综合位置/精确/具体/多样/长度,排序基于权重系统,比单纯字符串匹配准。

 上一页

区块链 web3 acp