--- title: "一步步带你搭建第一个 Easysearch 搜索服务" date: 2026-02-21 lastmod: 2026-02-21 tags: ["Easysearch", "搜索服务", "私有部署", "搜索引擎", "维基百科"] summary: "引言 # 你是否想过在自己的服务器上搭建一个功能强大的中文搜索引擎?比如搜索自己收藏的维基百科、文档库、或者知识库? 本文将手把手教你用 Python 和 Easysearch 搭建一个支持中文分词的私有维基百科搜索引擎。Easysearch 内置了中文分词的支持,这意味着我们不需要折腾复杂的中文分词配置,直接就能用! 在文章的最后,你将收获一下如下的私有搜索引擎: $ python3.10 search.py 大学 ============================================================ Easysearch 维基百科搜索 ============================================================ ... 找到 1 条结果 [1] Template:Potd/2006-1-2zh URL: https://zh.wikipedia.org/wiki/Template%3APotd/2006-1-2zh 高亮: ...伦敦帝国学院成立于1907年,是一个专精于科学技术的<em>大学</em>,它还是联邦<em>大学</em>伦敦<em>大学</em>的一个加盟学院。虽然它的正式全名是的“帝国科学,技术和医学学院”,自2002年来,它通常使用的英文名称是伦敦帝国学院。中文中,它通常被称为伦敦帝国理工学院或简称为帝国理工。... 准备工作 # 在开始之前,请确认: Easysearch 已在运行:假设你的 Easysearch 服务在 127.0.0.1:9200 运行,用户名为 admin,密码为 your_password(请根据实际情况修改)。 Python 3.10 环境: $ python3.10 --version Python 3.10.19 安装必要的 Python 库: python3.10 -m pip install "elasticsearch<7.14" wikiextractor 注意: - 必须使用 Python 3.10,因为 wikiextractor 库在后续 Python 版本中不能用。 - `elasticsearch<7." --- ## 引言 你是否想过在自己的服务器上搭建一个功能强大的中文搜索引擎?比如搜索自己收藏的维基百科、文档库、或者知识库? 本文将手把手教你用 Python 和 **Easysearch** 搭建一个支持中文分词的私有维基百科搜索引擎。Easysearch 内置了中文分词的支持,这意味着我们不需要折腾复杂的中文分词配置,直接就能用! 在文章的最后,你将收获一下如下的私有搜索引擎: ```shell $ python3.10 search.py 大学 ============================================================ Easysearch 维基百科搜索 ============================================================ ... 找到 1 条结果 [1] Template:Potd/2006-1-2zh URL: https://zh.wikipedia.org/wiki/Template%3APotd/2006-1-2zh 高亮: ...伦敦帝国学院成立于1907年,是一个专精于科学技术的大学,它还是联邦大学伦敦大学的一个加盟学院。虽然它的正式全名是的“帝国科学,技术和医学学院”,自2002年来,它通常使用的英文名称是伦敦帝国学院。中文中,它通常被称为伦敦帝国理工学院或简称为帝国理工。... ``` ## 准备工作 在开始之前,请确认: 1. **Easysearch 已在运行**:假设你的 Easysearch 服务在 `127.0.0.1:9200` 运行,用户名为 `admin`,密码为 `your_password`(请根据实际情况修改)。 2. **Python 3.10 环境**: ```bash $ python3.10 --version Python 3.10.19 ``` 3. **安装必要的 Python 库**: ```bash python3.10 -m pip install "elasticsearch<7.14" wikiextractor ``` **注意**: - 必须使用 Python 3.10,因为 wikiextractor 库在后续 Python 版本中不能用。 - `elasticsearch<7.14`:因为 Easysearch 基于 Elasticsearch 7.10.2,使用更高版本的 Python SDK 可能会出现兼容性问题。 ## 第一步:获取维基百科数据 中文维基百科定期发布完整的数据库转储文件。 下载链接:`https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles.xml.bz2` ```bash # 下载数据(约 3GB+,下载时间较长) wget https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles.xml.bz2 ``` ## 第二步:创建项目 创建一个 wiki_zh 的文件夹,将中文维基百科的数据放到文件夹下 ```shell $ mkdir wiki_zh $ cd wiki_zh $ ls -l .rw-r--r--@ 1 3.2G user staff 28 Jan 14:35 zhwiki-latest-pages-articles.xml.bz2 ``` ## 第三步:从 XML 中提取纯文本 JSON 维基百科的原始数据是 XML 格式,包含大量的模板和标记。我们使用 `wikiextractor` 库提取干净的文本。 ### 快速测试:提取 20 篇文章 使用我们提供的 Python 脚本,它利用 wikiextractor 的 Extractor 类进行文本清理: ```bash python3.10 extract_sample.py ``` 这个脚本会提取前 20 篇有效文章, 输出到 `wiki_data/AA/wiki_00.json`。 **脚本代码** (`extract_sample.py`): ```python #!/usr/bin/env python3.10 """ 使用 wikiextractor 库提取少量维基百科文章样本。 利用 wikiextractor 的 Extractor 类进行文本清理。 """ import bz2 import json import re import sys from pathlib import Path from io import StringIO from wikiextractor.extract import Extractor # =============================== # 配置参数 # =============================== OUTPUT_DIR = "./wiki_data" MAX_DOCUMENTS = 20 # 提取 20 篇文章用于快速测试 INPUT_FILE = "./zhwiki-latest-pages-articles.xml.bz2" URL_BASE = "https://zh.wikipedia.org/wiki/" # XML 标签正则表达式(来自 WikiExtractor) tagRE = re.compile(r'(.*?)<(/?\w+)[^>]*?(?:/>|>(.*?)(.*?))', re.DOTALL) def extract_wikipedia_sample(input_file, output_dir, max_docs=20): """使用 wikiextractor 的 Extractor 类提取前 N 个维基百科页面""" print(f"正在从 {input_file} 提取 {max_docs} 篇文档...") print("(使用 wikiextractor 的 Extractor 类)\n") # 创建输出目录 output_path = Path(output_dir) output_path.mkdir(exist_ok=True) # 创建子目录(wikiextractor 会创建嵌套目录,如 AA/, AB/ 等) subdir = output_path / "AA" subdir.mkdir(exist_ok=True) output_file = subdir / "wiki_00.json" # 配置 Extractor 以 JSON 格式输出 Extractor.to_json = True Extractor.keepLinks = False Extractor.keepSections = True Extractor.HtmlFormatting = False count = 0 documents = [] pages_scanned = 0 pages_skipped = 0 # 重定向 + 内容过短 # 解析 bz2 文件 with bz2.BZ2File(input_file, "rb") as f: page = [] page_id = '' revid = '' in_text = False redirect = False title = '' for line_num, line in enumerate(f): if count >= max_docs: print(f"\n✓ 已达到目标 {max_docs} 篇文档") break line_str = line.decode("utf-8", errors="ignore") # 快速路径:没有标签 if '<' not in line_str: if in_text: page.append(line_str) continue # 解析 XML 标签 m = tagRE.search(line_str) if not m: if in_text: page.append(line_str) continue tag = m.group(2) if tag == 'page': page = [] redirect = False title = '' page_id = '' revid = '' elif tag == 'id' and not page_id: page_id = m.group(3).strip() elif tag == 'id' and page_id: revid = m.group(3).strip() elif tag == 'title': title = m.group(3).strip() elif tag == 'redirect': redirect = True elif tag == 'text': in_text = True text_content = m.group(3) or m.group(4) or "" page.append(text_content) if m.lastindex == 4: # 自闭合标签 in_text = False elif tag == '/text': in_text = False text_content = m.group(3) or "" page.append(text_content) elif tag == '/page': pages_scanned += 1 # 处理完成的页面(跳过重定向) if not redirect and title and page: try: # 使用 wikiextractor 的 Extractor 类 extractor = Extractor(page_id, revid, URL_BASE, title, page) output_buffer = StringIO() # 使用 wikiextractor 的逻辑提取 extractor.extract(output_buffer, html_safe=False) # 获取 JSON 输出 json_str = output_buffer.getvalue().strip() if json_str: data = json.loads(json_str) # 只包含内容充实的文章 text_len = len(data.get('text', '')) if text_len > 100: documents.append(data) count += 1 print(f" [{count}/{max_docs}] ✓ {title} ({text_len} 字符)") if count >= max_docs: print(f"\n✓ 已达到目标 {max_docs} 篇文档") break else: pages_skipped += 1 except Exception as e: # 跳过有问题的页面 pages_skipped += 1 print(f" [错误] 处理失败: {title} - {e}") else: pages_skipped += 1 # 重置以处理下一页 page = [] page_id = '' revid = '' title = '' redirect = False # 统计摘要 print(f"\n统计信息:") print(f" 扫描页面总数: {pages_scanned}") print(f" 跳过 (重定向/过短/错误): {pages_skipped}") print(f" 成功提取: {count}") # 写入 JSONL 文档 print(f"\n正在将 {len(documents)} 篇文档写入 {output_file}...") with open(output_file, "w", encoding="utf-8") as f: for doc in documents: json.dump(doc, f, ensure_ascii=False) f.write("\n") print(f"✓ 完成!") print(f" 输出: {output_file}") return len(documents) if __name__ == "__main__": print("="*60) print("维基百科样本提取器 (使用 wikiextractor 库)") print("="*60) print() count = extract_wikipedia_sample(INPUT_FILE, OUTPUT_DIR, MAX_DOCUMENTS) print() print("="*60) print(f"✓ 成功提取 {count} 篇文档") print("="*60) ``` **输出结果**: ```plain ============================================================ 维基百科样本提取器 (使用 wikiextractor 库) ============================================================ 正在从 ./zhwiki-latest-pages-articles.xml.bz2 提取 20 篇文档... (使用 wikiextractor 的 Extractor 类) ... ✓ 已达到目标 20 篇文档 统计信息: 扫描页面总数: 149146 跳过 (重定向/过短/错误): 149126 成功提取: 20 正在将 20 篇文档写入 wiki_data/AA/wiki_00.json... ✓ 完成! 输出: wiki_data/AA/wiki_00.json ============================================================ ✓ 成功提取 20 篇文档 ============================================================ ``` 最终会在 `wiki_data/AA/wiki_00.json` 生成 JSONL 文件,每行是一篇维基百科文章: ```json {"id": "8572", "revid": "5313", "url": "https://zh.wikipedia.org/wiki/?curid=8572", "title": "MediaWiki:Gnunote", "text": "所有文本在..."} ... ``` --- ## 第四步:索引数据到 Easysearch 创建索引并将数据导入 Easysearch。 **脚本代码** (`index.py`): ```python #!/usr/bin/env python3.10 """ 将维基百科数据索引到 Easysearch。 创建带有中文分词器的索引并批量导入文档。 """ import json import urllib.parse from pathlib import Path from elasticsearch import Elasticsearch, helpers # =============================== # 配置参数 # =============================== ES_HOST = "https://127.0.0.1:9200" ES_USER = "admin" ES_PASSWORD = "your_password" INDEX_NAME = "wiki_zh" WIKI_DATA_DIR = "./wiki_data" MAX_DOCUMENTS = 20 # 索引的文档数量 # =============================== # 连接 Easysearch # =============================== def create_es_client(): """创建并返回 Elasticsearch 客户端""" client = Elasticsearch( [ES_HOST], http_auth=(ES_USER, ES_PASSWORD), # elasticsearch<7.14 使用 http_auth verify_certs=False, ssl_show_warn=False, ) if not client.ping(): raise ConnectionError(f"无法连接到 Easysearch: {ES_HOST}") print(f"已连接到 Easysearch: {ES_HOST}") return client # =============================== # 创建索引 # =============================== def create_index(client: Elasticsearch): """创建索引并配置中文分词器""" if client.indices.exists(index=INDEX_NAME): client.indices.delete(index=INDEX_NAME) print(f"已删除现有索引: {INDEX_NAME}") # 使用 ik_max_word 分词器配置索引映射 mapping = { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "content": { "type": "text", "analyzer": "ik_max_word" }, "url": { "type": "keyword" } } } client.indices.create(index=INDEX_NAME, body={"mappings": mapping}) print(f"已创建索引: {INDEX_NAME}") print(f" - title: text (ik_max_word)") print(f" - content: text (ik_max_word)") print(f" - url: keyword") # =============================== # 生成文档 # =============================== def generate_documents(): """读取 wiki_data 目录并生成待索引的文档""" count = 0 wiki_path = Path(WIKI_DATA_DIR) if not wiki_path.exists(): raise FileNotFoundError(f"找不到 wiki_data 目录: {WIKI_DATA_DIR}") # 递归查找所有 JSONL 文件 for json_file in wiki_path.rglob("*.json"): with open(json_file, "r", encoding="utf-8") as f: for line in f: if count >= MAX_DOCUMENTS: return line = line.strip() if not line: continue try: data = json.loads(line) title = data.get("title", "").strip() # wikiextractor 输出使用 'text' 键 content = data.get("text", "").strip() if not title or not content: continue # 构建维基百科 URL url_title = urllib.parse.quote(title.replace(" ", "_")) url = f"https://zh.wikipedia.org/wiki/{url_title}" count += 1 yield { "_index": INDEX_NAME, "_source": { "title": title, "content": content, "url": url } } except json.JSONDecodeError: continue # =============================== # 索引文档 # =============================== def index_documents(client: Elasticsearch): """使用 streaming_bulk 批量索引文档""" print(f"\n正在索引文档 (最多 {MAX_DOCUMENTS} 篇)...") success_count = 0 error_count = 0 for ok, info in helpers.streaming_bulk( client, generate_documents(), chunk_size=50, raise_on_error=False, raise_on_exception=False ): if ok: success_count += 1 else: error_count += 1 print(f" 错误: {info}") # 刷新索引使文档可被搜索 client.indices.refresh(index=INDEX_NAME) print(f"\n索引完成!") print(f" 成功: {success_count}") print(f" 错误: {error_count}") # =============================== # 主函数 # =============================== def main(): print("="*60) print("Easysearch 维基百科索引器") print("="*60) client = create_es_client() create_index(client) index_documents(client) print("="*60) print("完成!") print("="*60) if __name__ == "__main__": main() ``` 运行索引脚本: ```bash python3.10 index.py ``` 输出: ```plain ============================================================ Easysearch 维基百科索引器 ============================================================ 已连接到 Easysearch: https://127.0.0.1:9200 已创建索引: wiki_zh - title: text (ik_max_word) - content: text (ik_max_word) - url: keyword 正在索引文档 (最多 20 篇)... 索引完成! 成功: 20 错误: 0 ============================================================ 完成! ============================================================ ``` ## 第五步:搜索验证 创建独立的搜索脚本,支持命令行关键词和交互式搜索。 **脚本代码** (`search.py`): ```python #!/usr/bin/env python3.10 """ 在 Easysearch 中搜索维基百科索引并支持高亮显示。 """ from elasticsearch import Elasticsearch # =============================== # 配置参数 # =============================== ES_HOST = "https://127.0.0.1:9200" ES_USER = "admin" ES_PASSWORD = "your_password" INDEX_NAME = "wiki_zh" # =============================== # 连接 Easysearch # =============================== def create_es_client(): """创建并返回 Elasticsearch 客户端""" client = Elasticsearch( [ES_HOST], http_auth=(ES_USER, ES_PASSWORD), verify_certs=False, ssl_show_warn=False, ) if not client.ping(): raise ConnectionError(f"无法连接到 Easysearch: {ES_HOST}") return client # =============================== # 搜索并高亮 # =============================== def search(client: Elasticsearch, keyword: str, size: int = 5): """搜索关键词并返回带有高亮的结果""" query = { "query": { "match": { "content": keyword } }, "highlight": { "fields": { "content": { "fragment_size": 150, "number_of_fragments": 2 } } }, "size": size } response = client.search(index=INDEX_NAME, body=query) return response # =============================== # 显示结果 # =============================== def display_results(response, keyword: str): """格式化打印搜索结果""" total = response['hits']['total']['value'] hits = response['hits']['hits'] print(f"关键词: '{keyword}'") print(f"找到 {total} 条结果\n") for i, hit in enumerate(hits, 1): source = hit["_source"] highlights = hit.get("highlight", {}).get("content", []) print(f"[{i}] {source['title']}") print(f" URL: {source['url']}") print(f" 高亮:") for hl in highlights: print(f" ...{hl}...") print() # =============================== # 主函数 # =============================== def main(): import sys print("="*60) print("Easysearch 维基百科搜索") print("="*60) client = create_es_client() print(f"已连接到: {ES_HOST}") print(f"索引: {INDEX_NAME}\n") # 使用命令行关键词或默认关键词 keyword = sys.argv[1] if len(sys.argv) > 1 else "维基" print(f"搜索关键词: {keyword}\n") response = search(client, keyword) display_results(response, keyword) if __name__ == "__main__": main() ``` 运行搜索脚本: ```bash # 使用默认关键词 "维基" python3.10 search.py # 或指定关键词 python3.10 search.py 历史 ``` 输出示例: ```shell ============================================================ Easysearch 维基百科搜索 ============================================================ 已连接到: https://127.0.0.1:9200 索引: wiki_zh 搜索关键词: 维基 关键词: 维基 找到 1 条结果 [1] Template:Rfn URL: https://zh.wikipedia.org/wiki/Template%3ARfn 高亮: ...这里一般没有人对你的文章进行校阅哦,只要不违反原则的文字都没被保存下来。每个人都是平等的,大家都在参与贡献而已... ``` 注意搜索结果中的 `` 标签——这就是关键词高亮!在实际的 Web 应用中,你可以用 CSS 把这些标签渲染成黄色背景。 ## 总结 恭喜!你已经成功搭建了一个支持中文分词的搜索服务。回顾一下我们做了什么: 1. 获取中文维基百科数据 2. 使用 wikiextractor 库提取并清理文本 3. 用 Python 连接 Easysearch 4. 配置 `ik_max_word` 中文分词器 5. 使用 `streaming_bulk` 高效索引数据 6. 执行带高亮的搜索查询 ### 完整文件清单 | 文件 | 用途 | | --- | --- | | `zhwiki-latest-pages-articles.xml.bz2` | 中文维基百科数据 | | `extract_sample.py` | 提取 20 篇样本文档(使用 wikiextractor 库) | | `index.py` | 创建索引并导入数据 | | `search.py` | 搜索并显示高亮结果 | | `wiki_data/AA/wiki_00.json` | 提取后的维基百科数据 | ### 接下来 你可以: + 尝试搜索其他关键词,比如"人工智能"、"量子力学"等 + 修改 `extract_sampley.py`和 `index.py` ,提取并索引更多数据 + 开发一个 Web 前端,打造真正的搜索应用 祝你玩得开心!