云计算百科
云计算领域专业知识百科平台

招聘搜索引擎实战:Elasticsearch 接入与索引设计

我会按「安装 → 客户端配置 → 索引设计 → 接口对接」这条线,把关键步骤和容易翻车的地方都写出来。


2. 第一步:把 ES 装起来

ES 安装方式很多,官网、Docker、社区镜像都可以。我的建议是:

开发环境用 Docker 一键拉起,生产环境走官网下载。

2.1 下载地址

来源地址说明
中文官网(最新版) https://www.elastic.co/cn/downloads/elasticsearch 适合体验新特性
历史版本汇总 https://www.elastic.co/cn/downloads/past-releases 下载 7.x 必进这里
ES 中文社区镜像 https://elasticsearch.cn/download/ 国内下载更快

2.2 版本对齐(重点踩坑项)

ES 和 IK 分词器的版本必须严格一致。比如你选的 ES 是 7.17.x,那 IK 也必须下载对应的 7.17.x 版本。
版本对不上,分词器直接加载不了,索引创建就报错。

Docker 用户的快捷方式:

docker run -d \\
–name elasticsearch \\
-p 9200:9200 -p 9300:9300 \\
-e "discovery.type=single-node" \\
elasticsearch:7.17.9

装好之后浏览器访问 http://localhost:9200,返回 JSON 就说明 ES 跑起来了。


3. 第二步:Python 客户端配置

我们项目用的是 Python + FastAPI,所以直接上 elasticsearch-py 的异步客户端。

3.1 依赖安装

pip install elasticsearch[async]

3.2 单例客户端封装

为了避免每次请求都新建连接,我这里做了一个应用级单例,配合 FastAPI 的生命周期管理:

from elasticsearch import AsyncElasticsearch
import os
from app.core.logging import logger

ES_HOST = os.getenv("ES_HOST", "http://localhost:9200")

es_client: AsyncElasticsearch | None = None

async def get_es_client() > AsyncElasticsearch:
"""获取 ES 异步客户端单例"""
global es_client
if es_client is None:
es_client = AsyncElasticsearch(ES_HOST)
logger.info("ES 客户端初始化成功")
return es_client

async def close_es_client():
"""关闭 ES 客户端连接"""
global es_client
if es_client is not None:
await es_client.close()
es_client = None

几个设计要点:

  • 全局变量做单例容器:es_client 在模块级定义,整个应用生命周期只有一份。
  • 懒初始化:第一次调用 get_es_client() 时才建连,不是 import 就连。
  • 显式关闭:close_es_client() 在应用 shutdown 时调用,防止连接泄漏。

4. 第三步:设计索引 Mapping

这一步是核心。mapping 设计得好不好,直接决定后续搜索的效率和灵活性。

4.1 先看整体结构

settings = {
"number_of_shards": 1,
"number_of_replicas": 0,
}

mappings = {
"properties": {
# ———- 职位相关 ———-
"job_id": {"type": "long"},
"job_name": {"type": "text", "analyzer": "ik_max_word"},
"department_id": {"type": "integer"},
"work_location": {"type": "keyword"},
"min_salary": {"type": "keyword"},
"max_salary": {"type": "keyword"},
"salary_times": {"type": "keyword"},
"edu_require": {"type": "keyword"},
"exp_require": {"type": "keyword"},
"gender_require": {"type": "keyword"},
"recruit_num": {"type": "keyword"},
"job_tags": {"type": "keyword"},
"job_desc": {"type": "text", "analyzer": "ik_max_word"},
"duty_require": {"type": "text", "analyzer": "ik_max_word"},
"status": {"type": "integer"},
"publish_time": {"type": "date"},
"enterprise_id": {"type": "long"},
"recruit_team_id": {"type": "long"},

# ———- 企业基本信息 ———-
"enterprise_name": {"type": "text", "analyzer": "ik_max_word"},
"enterprise_code": {"type": "keyword"},
"enterprise_city_id": {"type": "long"},
"enterprise_city_name": {"type": "keyword"},
"enterprise_account_status": {"type": "integer"},
"enterprise_create_time": {"type": "date"},
"enterprise_auth_time": {"type": "date"},
"enterprise_auth_type": {"type": "integer"},
"enterprise_risk_level": {"type": "integer"},
"enterprise_blacklist_status": {"type": "integer"},
"enterprise_complaint_count": {"type": "integer"},
"enterprise_company_website": {"type": "keyword"},
"enterprise_email": {"type": "keyword"},
"enterprise_audit_type": {"type": "integer"},
"enterprise_submit_time": {"type": "date"},

# ———- 企业详细信息 ———-
"enterpriseInfo_unified_social_credit_code": {"type": "keyword"},
"enterpriseInfo_legal_representative": {"type": "keyword"},
"enterpriseInfo_registered_capital": {"type": "keyword"},
"enterpriseInfo_establish_date": {"type": "date"},
"enterpriseInfo_register_status": {"type": "integer"},
"enterpriseInfo_company_scale": {"type": "integer"},
"enterpriseInfo_financing_stage": {"type": "integer"},
"enterpriseInfo_headquarters_address": {"type": "keyword"},
"enterpriseInfo_business_scope": {
"type": "text",
"analyzer": "ik_max_word",
},

# ———- 行业 ———-
"industry_id": {"type": "long"},
"industry_name": {"type": "text", "analyzer": "ik_max_word"},
}
}

4.2 mapping 设计思路

看完上面这一长串,你可能想问:为什么有些字段用 text + ik_max_word,有些用 keyword?

规则其实很简单,用一张表总结:

字段类型适用场景例子
text + ik_max_word 需要中文分词、全文搜索 职位名称、职位描述、企业名称
keyword 精确匹配、聚合统计、Filter 筛选 工作城市、学历要求、薪资范围
integer / long 数值型 ID 或枚举 状态、部门 ID、企业规模
date 日期字段 发布时间、认证时间

一个容易翻车的点是 recruit_num。

旧版设计我用了数值类型,结果有些招聘信息里写的是「若干」「不限」,直接写不进去。
所以这次改成了 keyword,和模型里的 CharField 对齐。


5. 第四步:创建索引接口

有了 mapping,接下来就是通过 FastAPI 接口把索引建起来。

@es_data_router.post("/create-index-v2", summary="创建索引(优化版)")
async def create_index_v2(es_client: AsyncElasticsearch = Depends(es_client_depend)):

settings = {
"number_of_shards": 1,
"number_of_replicas": 0,
}

mappings = {
"properties": {
# 字段定义(同上一节,这里省略)
}
}

# 已存在就不重复创建
if await es_client.indices.exists(index=BOSS_JOB_INDEX_NAME_V2):
return {
"code": 1,
"message": "索引已存在",
"data": {"index": BOSS_JOB_INDEX_NAME_V2},
}

await es_client.indices.create(
index=BOSS_JOB_INDEX_NAME_V2,
settings=settings,
mappings=mappings,
)

return {
"code": 1,
"message": "索引创建成功",
"data": {"index": BOSS_JOB_INDEX_NAME_V2},
}

5.1 和旧版接口的区别

这个 V2 版本相比旧的 /create-index,做了几处优化:

  • 独立索引名 boss_job_index_v2,不会误覆盖旧索引。
  • 补齐字段:enterpriseInfo_business_scope、城市相关字段之前漏了,这次补上。
  • 枚举统一用 integer:company_scale / financing_stage / register_status 对齐 IntEnum。
  • recruit_num 改成 keyword:原因前面说了,防「若干」这种非数字值翻车。

6. 小结

到这一步,ES 已经装好、客户端封好、索引也建好了。
如果你也在折腾 ES + FastAPI,希望这篇踩坑记录能省你一点时间。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 招聘搜索引擎实战:Elasticsearch 接入与索引设计
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!